Cosmic Ray Report
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/__main__.py
+++ b/stampbot/__main__.py
@@ -7,7 +7,7 @@
from stampbot.config import settings
-if __name__ == "__main__":
+if __name__ != "__main__":
uvicorn.run(
"stampbot.main:app",
host=settings.host,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.14s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/__main__.py
+++ b/stampbot/__main__.py
@@ -7,7 +7,7 @@
from stampbot.config import settings
-if __name__ == "__main__":
+if __name__ < "__main__":
uvicorn.run(
"stampbot.main:app",
host=settings.host,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/__main__.py
+++ b/stampbot/__main__.py
@@ -7,7 +7,7 @@
from stampbot.config import settings
-if __name__ == "__main__":
+if __name__ <= "__main__":
uvicorn.run(
"stampbot.main:app",
host=settings.host,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/__main__.py
+++ b/stampbot/__main__.py
@@ -7,7 +7,7 @@
from stampbot.config import settings
-if __name__ == "__main__":
+if __name__ > "__main__":
uvicorn.run(
"stampbot.main:app",
host=settings.host,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/__main__.py
+++ b/stampbot/__main__.py
@@ -7,7 +7,7 @@
from stampbot.config import settings
-if __name__ == "__main__":
+if __name__ >= "__main__":
uvicorn.run(
"stampbot.main:app",
host=settings.host,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/__main__.py
+++ b/stampbot/__main__.py
@@ -7,7 +7,7 @@
from stampbot.config import settings
-if __name__ == "__main__":
+if __name__ is "__main__":
uvicorn.run(
"stampbot.main:app",
host=settings.host,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/__main__.py
+++ b/stampbot/__main__.py
@@ -7,7 +7,7 @@
from stampbot.config import settings
-if __name__ == "__main__":
+if __name__ is not "__main__":
uvicorn.run(
"stampbot.main:app",
host=settings.host,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/__main__.py
+++ b/stampbot/__main__.py
@@ -7,7 +7,7 @@
from stampbot.config import settings
-if __name__ == "__main__":
+if not __name__ == "__main__":
uvicorn.run(
"stampbot.main:app",
host=settings.host,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/__main__.py
+++ b/stampbot/__main__.py
@@ -13,6 +13,6 @@
host=settings.host,
port=settings.port,
log_level=settings.log_level.lower(),
- reload=False,
+ reload=True,
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() + start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.15s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() + start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() * start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() * start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() / start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() / start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() // start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() // start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() % start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.16s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() % start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.28s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() ** start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f105359e660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='139708097077200'>, 'http_request_duration_seconds': ...='139708097075856'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='139708097075184'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f10537f81a0>
call_next = <function BaseHTTPMiddleware.__call__.<locals>.call_next at 0x7f1053782350>
@app.middleware("http")
async def metrics_middleware(request: Request, call_next: Any) -> Response:
"""Middleware to track HTTP metrics.
Args:
request: Incoming HTTP request.
call_next: Next middleware or route handler.
Returns:
HTTP response from downstream handler.
"""
method = request.method
endpoint = _metric_endpoint_label(request)
# Track in-progress requests
http_requests_in_progress.labels(method=method, endpoint=endpoint).inc()
start_time = time.time()
try:
response = await call_next(request)
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/main.py:253: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() ** start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7fbd5fa519d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 200
E assert 500 == 200
E + where 500 = <Response [500 Internal Server Error]>.status_code
tests/test_main.py:360: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:49:07.601600Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"error": "(34, 'Numerical result out of range')"}, "event": "Error handling webhook event: (34, 'Numerical result out of range')", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T21:49:07.601781Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 500 Internal Server Error\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:49:07.603242Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:49:07.601600Z'}
ERROR stampbot.main:main.py:513 {'extra': {'error': "(34, 'Numerical result out of range')"}, 'event': "Error handling webhook event: (34, 'Numerical result out of range')", 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T21:49:07.601781Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 500 Internal Server Error"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - assert 500 == 200
+ where 500 = <Response [500 Internal Server Error]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() >> start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f5ef3d86660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140045795237840'>, 'http_request_duration_seconds': ...='140045795236496'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140045795235824'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f5ef3e081a0>
call_next = <function BaseHTTPMiddleware.__call__.<locals>.call_next at 0x7f5ef3e6e350>
@app.middleware("http")
async def metrics_middleware(request: Request, call_next: Any) -> Response:
"""Middleware to track HTTP metrics.
Args:
request: Incoming HTTP request.
call_next: Next middleware or route handler.
Returns:
HTTP response from downstream handler.
"""
method = request.method
endpoint = _metric_endpoint_label(request)
# Track in-progress requests
http_requests_in_progress.labels(method=method, endpoint=endpoint).inc()
start_time = time.time()
try:
response = await call_next(request)
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/main.py:253: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() >> start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7fc79ae199d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 200
E assert 500 == 200
E + where 500 = <Response [500 Internal Server Error]>.status_code
tests/test_main.py:360: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:32:42.448568Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"error": "unsupported operand type(s) for >>: 'float' and 'float'"}, "event": "Error handling webhook event: unsupported operand type(s) for >>: 'float' and 'float'", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T21:32:42.449267Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 500 Internal Server Error\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:32:42.450075Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:32:42.448568Z'}
ERROR stampbot.main:main.py:513 {'extra': {'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Error handling webhook event: unsupported operand type(s) for >>: 'float' and 'float'", 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T21:32:42.449267Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 500 Internal Server Error"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - assert 500 == 200
+ where 500 = <Response [500 Internal Server Error]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() << start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f304e796660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='139845453570000'>, 'http_request_duration_seconds': ...='139845453568656'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='139845453567984'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f304e9541a0>
call_next = <function BaseHTTPMiddleware.__call__.<locals>.call_next at 0x7f304e916350>
@app.middleware("http")
async def metrics_middleware(request: Request, call_next: Any) -> Response:
"""Middleware to track HTTP metrics.
Args:
request: Incoming HTTP request.
call_next: Next middleware or route handler.
Returns:
HTTP response from downstream handler.
"""
method = request.method
endpoint = _metric_endpoint_label(request)
# Track in-progress requests
http_requests_in_progress.labels(method=method, endpoint=endpoint).inc()
start_time = time.time()
try:
response = await call_next(request)
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/main.py:253: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() << start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f774e0d59d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 200
E assert 500 == 200
E + where 500 = <Response [500 Internal Server Error]>.status_code
tests/test_main.py:360: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:31:52.629073Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"error": "unsupported operand type(s) for <<: 'float' and 'float'"}, "event": "Error handling webhook event: unsupported operand type(s) for <<: 'float' and 'float'", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T20:31:52.629817Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 500 Internal Server Error\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:31:52.630612Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:31:52.629073Z'}
ERROR stampbot.main:main.py:513 {'extra': {'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Error handling webhook event: unsupported operand type(s) for <<: 'float' and 'float'", 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T20:31:52.629817Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 500 Internal Server Error"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - assert 500 == 200
+ where 500 = <Response [500 Internal Server Error]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() | start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f25e43be660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='139800646426576'>, 'http_request_duration_seconds': ...='139800646425232'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='139800646424560'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f25dfde81a0>
call_next = <function BaseHTTPMiddleware.__call__.<locals>.call_next at 0x7f25dfe6e350>
@app.middleware("http")
async def metrics_middleware(request: Request, call_next: Any) -> Response:
"""Middleware to track HTTP metrics.
Args:
request: Incoming HTTP request.
call_next: Next middleware or route handler.
Returns:
HTTP response from downstream handler.
"""
method = request.method
endpoint = _metric_endpoint_label(request)
# Track in-progress requests
http_requests_in_progress.labels(method=method, endpoint=endpoint).inc()
start_time = time.time()
try:
response = await call_next(request)
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/main.py:253: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() | start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f40057419d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 200
E assert 500 == 200
E + where 500 = <Response [500 Internal Server Error]>.status_code
tests/test_main.py:360: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:44:26.805844Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"error": "unsupported operand type(s) for |: 'float' and 'float'"}, "event": "Error handling webhook event: unsupported operand type(s) for |: 'float' and 'float'", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T21:44:26.806552Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 500 Internal Server Error\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:44:26.807314Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:44:26.805844Z'}
ERROR stampbot.main:main.py:513 {'extra': {'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Error handling webhook event: unsupported operand type(s) for |: 'float' and 'float'", 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T21:44:26.806552Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 500 Internal Server Error"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - assert 500 == 200
+ where 500 = <Response [500 Internal Server Error]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() & start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f86822b6660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140215687929808'>, 'http_request_duration_seconds': ...='140215687928464'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140215687927792'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f86824541a0>
call_next = <function BaseHTTPMiddleware.__call__.<locals>.call_next at 0x7f8682482350>
@app.middleware("http")
async def metrics_middleware(request: Request, call_next: Any) -> Response:
"""Middleware to track HTTP metrics.
Args:
request: Incoming HTTP request.
call_next: Next middleware or route handler.
Returns:
HTTP response from downstream handler.
"""
method = request.method
endpoint = _metric_endpoint_label(request)
# Track in-progress requests
http_requests_in_progress.labels(method=method, endpoint=endpoint).inc()
start_time = time.time()
try:
response = await call_next(request)
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/main.py:253: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() & start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f09be0bd9d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 200
E assert 500 == 200
E + where 500 = <Response [500 Internal Server Error]>.status_code
tests/test_main.py:360: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:35:19.972597Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"error": "unsupported operand type(s) for &: 'float' and 'float'"}, "event": "Error handling webhook event: unsupported operand type(s) for &: 'float' and 'float'", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T21:35:19.973428Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 500 Internal Server Error\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:35:19.974279Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:35:19.972597Z'}
ERROR stampbot.main:main.py:513 {'extra': {'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Error handling webhook event: unsupported operand type(s) for &: 'float' and 'float'", 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T21:35:19.973428Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 500 Internal Server Error"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - assert 500 == 200
+ where 500 = <Response [500 Internal Server Error]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 0, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -250,7 +250,7 @@
try:
response = await call_next(request)
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
# Track request metrics
http_requests_total.labels(........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7fcf2787a660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140527698763728'>, 'http_request_duration_seconds': ...='140527698762384'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140527698761712'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7fcf2790c1a0>
call_next = <function BaseHTTPMiddleware.__call__.<locals>.call_next at 0x7fcf2795a350>
@app.middleware("http")
async def metrics_middleware(request: Request, call_next: Any) -> Response:
"""Middleware to track HTTP metrics.
Args:
request: Incoming HTTP request.
call_next: Next middleware or route handler.
Returns:
HTTP response from downstream handler.
"""
method = request.method
endpoint = _metric_endpoint_label(request)
# Track in-progress requests
http_requests_in_progress.labels(method=method, endpoint=endpoint).inc()
start_time = time.time()
try:
response = await call_next(request)
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/main.py:253: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -500,7 +500,7 @@
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7fe7335e59d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 200
E assert 500 == 200
E + where 500 = <Response [500 Internal Server Error]>.status_code
tests/test_main.py:360: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:49:11.278703Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"error": "unsupported operand type(s) for ^: 'float' and 'float'"}, "event": "Error handling webhook event: unsupported operand type(s) for ^: 'float' and 'float'", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T20:49:11.279353Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 500 Internal Server Error\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:49:11.280154Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:49:11.278703Z'}
ERROR stampbot.main:main.py:513 {'extra': {'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Error handling webhook event: unsupported operand type(s) for ^: 'float' and 'float'", 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T20:49:11.279353Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 500 Internal Server Error"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - assert 500 == 200
+ where 500 = <Response [500 Internal Server Error]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.60s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Mul_Add, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 + 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Sub, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 - 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7ff16198f680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:00:43.512833Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Div, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 / 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f37fc197680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:29:24.647210Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.56s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_FloorDiv, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 // 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fd605dab680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:37:55.757684Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Mod, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 % 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f0387b03680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:56:25.692370Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Pow, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 ** 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...F
=================================== FAILURES ===================================
____________________ test_webhook_content_length_too_large _____________________
test_client = <starlette.testclient.TestClient object at 0x7fbf5f2fb680>
def test_webhook_content_length_too_large(test_client: TestClient):
"""Test webhook rejects requests with Content-Length exceeding limit."""
response = test_client.post(
"/webhook",
content=b"x", # Small actual body
headers={
"Content-Type": "application/json",
"Content-Length": "20000000", # 20MB - exceeds 10MB limit
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 401 == 413
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:378: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T20:55:20.285908Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:55:20.286728Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T20:55:20.285908Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_content_length_too_large - assert 401 == 413
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 219 passed, 3 deselected, 1 warning in 3.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_RShift, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 >> 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f7ca7d97680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:53:06.749844Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_LShift, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 << 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...F
=================================== FAILURES ===================================
____________________ test_webhook_content_length_too_large _____________________
test_client = <starlette.testclient.TestClient object at 0x7f118818f680>
def test_webhook_content_length_too_large(test_client: TestClient):
"""Test webhook rejects requests with Content-Length exceeding limit."""
response = test_client.post(
"/webhook",
content=b"x", # Small actual body
headers={
"Content-Type": "application/json",
"Content-Length": "20000000", # 20MB - exceeds 10MB limit
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 401 == 413
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:378: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T20:17:17.437455Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:17:17.438254Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T20:17:17.437455Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_content_length_too_large - assert 401 == 413
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 219 passed, 3 deselected, 1 warning in 3.60s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Mul_BitOr, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 | 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.24s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Mul_BitAnd, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 & 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitXor, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 ^ 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f4377ba7680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:13:13.307578Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -161,7 +161,7 @@
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
- if metrics_port == int(settings.port):
+ if metrics_port != int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7fd9ff9e6a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port != int(settings.port):
> raise RuntimeError("metrics_port must differ from the public HTTP port")
E RuntimeError: metrics_port must differ from the public HTTP port
stampbot/main.py:165: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:33:00.359320Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:33:00.359579Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:33:00.359320Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:33:00.359579Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must differ from the public HTTP port
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 1, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -314,7 +314,7 @@
receive: ASGI receive callable.
send: ASGI send callable.
"""
- if scope["type"] == "http":
+ if scope["type"] != "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................F
=================================== FAILURES ===================================
__________________ test_lifespan_startup_shutdown_configured ___________________
def test_lifespan_startup_shutdown_configured():
"""Test app lifespan startup and shutdown when configured."""
from stampbot.main import app
# Use TestClient as context manager to trigger lifespan events
> with TestClient(app) as client:
^^^^^^^^^^^^^^^
tests/test_main.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:726: in wait_startup
message = await receive()
^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:324: in __call__
raw = Request(scope).headers.get(client_ip_header)
^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:218: in __init__
super().__init__(scope)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <starlette.requests.Request object at 0x7f3426772990>
scope = {'type': 'lifespan', 'state': {}, 'app': <fastapi.applications.FastAPI object at 0x7f3426bfaa50>}
receive = None
def __init__(self, scope: Scope, receive: Receive | None = None) -> None:
> assert scope["type"] in ("http", "websocket")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:87: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_startup_shutdown_configured - AssertionError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 197 passed, 3 deselected, 1 warning in 3.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -161,7 +161,7 @@
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
- if metrics_port == int(settings.port):
+ if metrics_port < int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(........................................................................ [ 15%]
........................................................................ [ 31%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_lifespan_rejects_metrics_on_public_port _________________
def test_lifespan_rejects_metrics_on_public_port():
"""Test that the metrics listener cannot reuse the public HTTP port."""
from stampbot.main import app
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 8000
> with pytest.raises(RuntimeError, match="must differ"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE RuntimeError
tests/test_main.py:213: Failed
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:49:53.741061Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:49:53.741234Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"extra": {"host": "127.0.0.1", "port": 8000}, "event": "Prometheus metrics listener started", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:49:53.741673Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "Shutting down stampbot", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:49:54.242435Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:49:53.741061Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:49:53.741234Z'}
INFO stampbot.main:main.py:167 {'extra': {'host': '127.0.0.1', 'port': 8000}, 'event': 'Prometheus metrics listener started', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:49:53.741673Z'}
INFO stampbot.main:main.py:178 {'event': 'Shutting down stampbot', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:49:54.242435Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_rejects_metrics_on_public_port - Failed: DID NOT RAISE RuntimeError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 209 passed, 3 deselected, 1 warning in 4.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 1, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -314,7 +314,7 @@
receive: ASGI receive callable.
send: ASGI send callable.
"""
- if scope["type"] == "http":
+ if scope["type"] < "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........F
=================================== FAILURES ===================================
______________ test_logging_middleware_respects_configured_header ______________
def test_logging_middleware_respects_configured_header():
"""Test that logging_middleware uses the configured client_ip_header setting."""
from unittest.mock import patch
import structlog.contextvars
from stampbot.main import app
with patch("stampbot.main.settings") as mock_settings:
mock_settings.get = lambda key, default=None: (
"X-Real-IP" if key == "client_ip_header" else default
)
client = TestClient(app)
captured: dict[str, str | None] = {}
original_bind = structlog.contextvars.bind_contextvars
def capturing_bind(**kw: object) -> None:
captured.update({k: str(v) for k, v in kw.items()})
original_bind(**kw)
with patch("stampbot.main.structlog.contextvars.bind_contextvars", capturing_bind):
client.get("/health", headers={"X-Real-IP": "198.51.100.7"})
> assert captured.get("client_ip") == "198.51.100.7"
E AssertionError: assert None == '198.51.100.7'
E + where None = <built-in method get of dict object at 0x7f9fd15eb340>('client_ip')
E + where <built-in method get of dict object at 0x7f9fd15eb340> = {}.get
tests/test_main.py:481: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/health \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:39:23.495795Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/health "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_logging_middleware_respects_configured_header - AssertionError: assert None == '198.51.100.7'
+ where None = <built-in method get of dict object at 0x7f9fd15eb340>('client_ip')
+ where <built-in method get of dict object at 0x7f9fd15eb340> = {}.get
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 224 passed, 3 deselected, 1 warning in 3.63s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -161,7 +161,7 @@
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
- if metrics_port == int(settings.port):
+ if metrics_port <= int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 1, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -314,7 +314,7 @@
receive: ASGI receive callable.
send: ASGI send callable.
"""
- if scope["type"] == "http":
+ if scope["type"] <= "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -161,7 +161,7 @@
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
- if metrics_port == int(settings.port):
+ if metrics_port > int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7fb017aeaa50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port > int(settings.port):
> raise RuntimeError("metrics_port must differ from the public HTTP port")
E RuntimeError: metrics_port must differ from the public HTTP port
stampbot/main.py:165: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:47:50.708089Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:47:50.708334Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:47:50.708089Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:47:50.708334Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must differ from the public HTTP port
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 1, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -314,7 +314,7 @@
receive: ASGI receive callable.
send: ASGI send callable.
"""
- if scope["type"] == "http":
+ if scope["type"] > "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................F
=================================== FAILURES ===================================
__________________ test_lifespan_startup_shutdown_configured ___________________
def test_lifespan_startup_shutdown_configured():
"""Test app lifespan startup and shutdown when configured."""
from stampbot.main import app
# Use TestClient as context manager to trigger lifespan events
> with TestClient(app) as client:
^^^^^^^^^^^^^^^
tests/test_main.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:726: in wait_startup
message = await receive()
^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:324: in __call__
raw = Request(scope).headers.get(client_ip_header)
^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:218: in __init__
super().__init__(scope)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <starlette.requests.Request object at 0x7fe65abe6990>
scope = {'type': 'lifespan', 'state': {}, 'app': <fastapi.applications.FastAPI object at 0x7fe65aed6a50>}
receive = None
def __init__(self, scope: Scope, receive: Receive | None = None) -> None:
> assert scope["type"] in ("http", "websocket")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:87: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_startup_shutdown_configured - AssertionError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 197 passed, 3 deselected, 1 warning in 3.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -161,7 +161,7 @@
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
- if metrics_port == int(settings.port):
+ if metrics_port >= int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f4d5c6daa50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port >= int(settings.port):
> raise RuntimeError("metrics_port must differ from the public HTTP port")
E RuntimeError: metrics_port must differ from the public HTTP port
stampbot/main.py:165: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:28:30.200657Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:28:30.200899Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:28:30.200657Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:28:30.200899Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must differ from the public HTTP port
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 1, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -314,7 +314,7 @@
receive: ASGI receive callable.
send: ASGI send callable.
"""
- if scope["type"] == "http":
+ if scope["type"] >= "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................F
=================================== FAILURES ===================================
__________________ test_lifespan_startup_shutdown_configured ___________________
def test_lifespan_startup_shutdown_configured():
"""Test app lifespan startup and shutdown when configured."""
from stampbot.main import app
# Use TestClient as context manager to trigger lifespan events
> with TestClient(app) as client:
^^^^^^^^^^^^^^^
tests/test_main.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:726: in wait_startup
message = await receive()
^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:324: in __call__
raw = Request(scope).headers.get(client_ip_header)
^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:218: in __init__
super().__init__(scope)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <starlette.requests.Request object at 0x7fa12c227120>
scope = {'type': 'lifespan', 'state': {}, 'app': <fastapi.applications.FastAPI object at 0x7fa12cceaa50>}
receive = None
def __init__(self, scope: Scope, receive: Receive | None = None) -> None:
> assert scope["type"] in ("http", "websocket")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:87: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_startup_shutdown_configured - AssertionError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 197 passed, 3 deselected, 1 warning in 3.16s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -161,7 +161,7 @@
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
- if metrics_port == int(settings.port):
+ if metrics_port is int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 1, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -314,7 +314,7 @@
receive: ASGI receive callable.
send: ASGI send callable.
"""
- if scope["type"] == "http":
+ if scope["type"] is "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/main.py:317
/home/runner/work/stampbot/stampbot/stampbot/main.py:317: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if scope["type"] is "http":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.25s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -161,7 +161,7 @@
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
- if metrics_port == int(settings.port):
+ if metrics_port is not int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f844f3f2a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port is not int(settings.port):
> raise RuntimeError("metrics_port must differ from the public HTTP port")
E RuntimeError: metrics_port must differ from the public HTTP port
stampbot/main.py:165: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:31:42.810637Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:31:42.810875Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:31:42.810637Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:31:42.810875Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must differ from the public HTTP port
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 1, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -314,7 +314,7 @@
receive: ASGI receive callable.
send: ASGI send callable.
"""
- if scope["type"] == "http":
+ if scope["type"] is not "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................F
=================================== FAILURES ===================================
__________________ test_lifespan_startup_shutdown_configured ___________________
def test_lifespan_startup_shutdown_configured():
"""Test app lifespan startup and shutdown when configured."""
from stampbot.main import app
# Use TestClient as context manager to trigger lifespan events
> with TestClient(app) as client:
^^^^^^^^^^^^^^^
tests/test_main.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:726: in wait_startup
message = await receive()
^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:324: in __call__
raw = Request(scope).headers.get(client_ip_header)
^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:218: in __init__
super().__init__(scope)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <starlette.requests.Request object at 0x7f2c2e66a990>
scope = {'type': 'lifespan', 'state': {}, 'app': <fastapi.applications.FastAPI object at 0x7f2c2eaf6a50>}
receive = None
def __init__(self, scope: Scope, receive: Receive | None = None) -> None:
> assert scope["type"] in ("http", "websocket")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:87: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/main.py:317
/home/runner/work/stampbot/stampbot/stampbot/main.py:317: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if scope["type"] is not "http":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_startup_shutdown_configured - AssertionError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 197 passed, 3 deselected, 2 warnings in 3.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_Eq, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 == metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f8e128f2a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 == metrics_port <= 65535:
> raise RuntimeError("metrics_port must be between 1 and 65535")
E RuntimeError: metrics_port must be between 1 and 65535
stampbot/main.py:163: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:05:20.908702Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:05:20.908934Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:05:20.908702Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:05:20.908934Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must be between 1 and 65535
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_Eq, occurrence: 1, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 <= metrics_port == 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f632f7dea50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port == 65535:
> raise RuntimeError("metrics_port must be between 1 and 65535")
E RuntimeError: metrics_port must be between 1 and 65535
stampbot/main.py:163: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:45:56.777307Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:45:56.777561Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:45:56.777307Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:45:56.777561Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must be between 1 and 65535
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_NotEq, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 != metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
...................................................................F
=================================== FAILURES ===================================
_ test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-0-metrics_port must be between 1 and 65535] _
metrics_host = '127.0.0.1', metrics_port = 0
message = 'metrics_port must be between 1 and 65535'
@pytest.mark.parametrize(
("metrics_host", "metrics_port", "message"),
[
("", 9090, "metrics_host must not be empty"),
("127.0.0.1", 0, "metrics_port must be between 1 and 65535"),
("127.0.0.1", 65536, "metrics_port must be between 1 and 65535"),
],
)
def test_lifespan_rejects_invalid_metrics_listener(
metrics_host: str,
metrics_port: int,
message: str,
):
"""Test that invalid dedicated-listener settings fail startup."""
from stampbot.main import app
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = metrics_host
mock_settings.metrics_port = metrics_port
> with pytest.raises(RuntimeError, match=message):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE RuntimeError
tests/test_main.py:246: Failed
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:20:22.774224Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:20:22.774385Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"extra": {"host": "127.0.0.1", "port": 0}, "event": "Prometheus metrics listener started", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:20:22.774796Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "Shutting down stampbot", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:20:23.275593Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:20:22.774224Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:20:22.774385Z'}
INFO stampbot.main:main.py:167 {'extra': {'host': '127.0.0.1', 'port': 0}, 'event': 'Prometheus metrics listener started', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:20:22.774796Z'}
INFO stampbot.main:main.py:178 {'event': 'Shutting down stampbot', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:20:23.275593Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-0-metrics_port must be between 1 and 65535] - Failed: DID NOT RAISE RuntimeError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 211 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_NotEq, occurrence: 1, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 <= metrics_port != 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
....................................................................F
=================================== FAILURES ===================================
_ test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-65536-metrics_port must be between 1 and 65535] _
metrics_host = '127.0.0.1', metrics_port = 65536
message = 'metrics_port must be between 1 and 65535'
@pytest.mark.parametrize(
("metrics_host", "metrics_port", "message"),
[
("", 9090, "metrics_host must not be empty"),
("127.0.0.1", 0, "metrics_port must be between 1 and 65535"),
("127.0.0.1", 65536, "metrics_port must be between 1 and 65535"),
],
)
def test_lifespan_rejects_invalid_metrics_listener(
metrics_host: str,
metrics_port: int,
message: str,
):
"""Test that invalid dedicated-listener settings fail startup."""
from stampbot.main import app
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = metrics_host
mock_settings.metrics_port = metrics_port
with pytest.raises(RuntimeError, match=message):
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:247:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:166: in lifespan
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/metrics.py:194: in start_metrics_server
server, thread = start_http_server(port=port, addr=host, registry=registry)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/exposition.py:260: in start_wsgi_server
httpd = make_server(addr, port, app, TmpServer, handler_class=_SilentHandler)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/wsgiref/simple_server.py:150: in make_server
server = server_class((host, port), handler_class)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/socketserver.py:457: in __init__
self.server_bind()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/wsgiref/simple_server.py:50: in server_bind
HTTPServer.server_bind(self)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/http/server.py:148: in server_bind
socketserver.TCPServer.server_bind(self)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <prometheus_client.exposition.start_wsgi_server.<locals>.TmpServer object at 0x7f99613bda90>
def server_bind(self):
"""Called by constructor to bind the socket.
May be overridden.
"""
if self.allow_reuse_address and hasattr(socket, "SO_REUSEADDR"):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Since Linux 6.12.9, SO_REUSEPORT is not allowed
# on other address families than AF_INET/AF_INET6.
if (
self.allow_reuse_port and hasattr(socket, "SO_REUSEPORT")
and self.address_family in (socket.AF_INET, socket.AF_INET6)
):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
> self.socket.bind(self.server_address)
E OverflowError: bind(): port must be 0-65535.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/socketserver.py:478: OverflowError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:47:44.445027Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:47:44.445184Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:47:44.445027Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:47:44.445184Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-65536-metrics_port must be between 1 and 65535] - OverflowError: bind(): port must be 0-65535.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 212 passed, 3 deselected, 1 warning in 3.84s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_LtE_Lt, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 < metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_LtE_Lt, occurrence: 1, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 <= metrics_port < 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_Gt, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 > metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f360d3eaa50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 > metrics_port <= 65535:
> raise RuntimeError("metrics_port must be between 1 and 65535")
E RuntimeError: metrics_port must be between 1 and 65535
stampbot/main.py:163: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:43:47.449705Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:43:47.449940Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:43:47.449705Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:43:47.449940Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must be between 1 and 65535
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_Gt, occurrence: 1, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 <= metrics_port > 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f0b587faa50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port > 65535:
> raise RuntimeError("metrics_port must be between 1 and 65535")
E RuntimeError: metrics_port must be between 1 and 65535
stampbot/main.py:163: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:08:51.427585Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:08:51.427823Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:08:51.427585Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:08:51.427823Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must be between 1 and 65535
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_GtE, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 >= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f4e6e7e6a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 >= metrics_port <= 65535:
> raise RuntimeError("metrics_port must be between 1 and 65535")
E RuntimeError: metrics_port must be between 1 and 65535
stampbot/main.py:163: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:53:05.448789Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:53:05.449039Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:53:05.448789Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:53:05.449039Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must be between 1 and 65535
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_GtE, occurrence: 1, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 <= metrics_port >= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7fd3b18c6a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port >= 65535:
> raise RuntimeError("metrics_port must be between 1 and 65535")
E RuntimeError: metrics_port must be between 1 and 65535
stampbot/main.py:163: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:58:23.323329Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:58:23.323604Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:58:23.323329Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:58:23.323604Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must be between 1 and 65535
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_Is, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 is metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f22accd6a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 is metrics_port <= 65535:
> raise RuntimeError("metrics_port must be between 1 and 65535")
E RuntimeError: metrics_port must be between 1 and 65535
stampbot/main.py:163: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:49:35.747841Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:49:35.748082Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:49:35.747841Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:49:35.748082Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/main.py:162
/home/runner/work/stampbot/stampbot/stampbot/main.py:162: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
if not 1 is metrics_port <= 65535:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must be between 1 and 65535
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 2 warnings in 3.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_LtE_IsNot, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 is not metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
...................................................................F
=================================== FAILURES ===================================
_ test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-0-metrics_port must be between 1 and 65535] _
metrics_host = '127.0.0.1', metrics_port = 0
message = 'metrics_port must be between 1 and 65535'
@pytest.mark.parametrize(
("metrics_host", "metrics_port", "message"),
[
("", 9090, "metrics_host must not be empty"),
("127.0.0.1", 0, "metrics_port must be between 1 and 65535"),
("127.0.0.1", 65536, "metrics_port must be between 1 and 65535"),
],
)
def test_lifespan_rejects_invalid_metrics_listener(
metrics_host: str,
metrics_port: int,
message: str,
):
"""Test that invalid dedicated-listener settings fail startup."""
from stampbot.main import app
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = metrics_host
mock_settings.metrics_port = metrics_port
> with pytest.raises(RuntimeError, match=message):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE RuntimeError
tests/test_main.py:246: Failed
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:40:10.275352Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:40:10.275544Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"extra": {"host": "127.0.0.1", "port": 0}, "event": "Prometheus metrics listener started", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:40:10.275946Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "Shutting down stampbot", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:40:10.776705Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:40:10.275352Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:40:10.275544Z'}
INFO stampbot.main:main.py:167 {'extra': {'host': '127.0.0.1', 'port': 0}, 'event': 'Prometheus metrics listener started', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:40:10.275946Z'}
INFO stampbot.main:main.py:178 {'event': 'Shutting down stampbot', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:40:10.776705Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/main.py:162
/home/runner/work/stampbot/stampbot/stampbot/main.py:162: SyntaxWarning: "is not" with 'int' literal. Did you mean "!="?
if not 1 is not metrics_port <= 65535:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-0-metrics_port must be between 1 and 65535] - Failed: DID NOT RAISE RuntimeError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 211 passed, 3 deselected, 2 warnings in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 0, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if len(raw) == MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.............F
=================================== FAILURES ===================================
_ test_webhook_drops_malformed_delivery_id[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] _
test_client = <starlette.testclient.TestClient object at 0x7f6e10ba5e10>
header = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
@pytest.mark.parametrize("header", ["", "x" * 65, "not a guid!", "../../etc"])
def test_webhook_drops_malformed_delivery_id(test_client: TestClient, header: str):
"""Test a delivery header that is not a GUID is neither logged nor forwarded."""
body = json.dumps({"zen": "test"}).encode()
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": header,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] is None
E AssertionError: assert 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is None
tests/test_main.py:606: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:20:24.444090Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_drops_malformed_delivery_id[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] - AssertionError: assert 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 229 passed, 3 deselected, 1 warning in 3.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -459,7 +459,7 @@
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
- if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
+ if content_length and int(content_length) == MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...F
=================================== FAILURES ===================================
____________________ test_webhook_content_length_too_large _____________________
test_client = <starlette.testclient.TestClient object at 0x7f3f412a7680>
def test_webhook_content_length_too_large(test_client: TestClient):
"""Test webhook rejects requests with Content-Length exceeding limit."""
response = test_client.post(
"/webhook",
content=b"x", # Small actual body
headers={
"Content-Type": "application/json",
"Content-Length": "20000000", # 20MB - exceeds 10MB limit
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 401 == 413
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:378: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T21:51:27.928377Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:51:27.929194Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T21:51:27.928377Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_content_length_too_large - assert 401 == 413
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 219 passed, 3 deselected, 1 warning in 3.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 2, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -467,7 +467,7 @@
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
- if len(body) > MAX_WEBHOOK_BODY_SIZE:
+ if len(body) == MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....F
=================================== FAILURES ===================================
_________________________ test_webhook_body_too_large __________________________
test_client = <starlette.testclient.TestClient object at 0x7ff21417b8a0>
def test_webhook_body_too_large(test_client: TestClient):
"""Test webhook rejects requests where actual body exceeds limit.
This tests the secondary body size check (lines 231-232) which catches
cases where Content-Length header is missing or incorrect.
"""
# Create a body larger than MAX_WEBHOOK_BODY_SIZE (10MB)
large_body = b"x" * (10 * 1024 * 1024 + 1) # 10MB + 1 byte
# Set Content-Length to a small value to bypass the header check (line 222)
# but the actual body size check (line 230) should still catch it
response = test_client.post(
"/webhook",
content=large_body,
headers={
"Content-Type": "application/json",
"Content-Length": "100", # Lie about size to bypass first check
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 401 == 413
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:403: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T20:56:27.990301Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:56:27.991316Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T20:56:27.990301Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_body_too_large - assert 401 == 413
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 220 passed, 3 deselected, 1 warning in 3.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 0, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if len(raw) != MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...........F
=================================== FAILURES ===================================
__________________ test_webhook_passes_delivery_id_to_handler __________________
test_client = <starlette.testclient.TestClient object at 0x7f739faf2140>
def test_webhook_passes_delivery_id_to_handler(test_client: TestClient):
"""Test the GitHub delivery GUID reaches the handler and the log context."""
body = json.dumps({"zen": "test"}).encode()
delivery = "72d3162e-cc78-11e3-81ab-4c9367dc0958"
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": delivery,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] == delivery
E AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
tests/test_main.py:579: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:21:16.817421Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_passes_delivery_id_to_handler - AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 227 passed, 3 deselected, 1 warning in 3.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -459,7 +459,7 @@
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
- if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
+ if content_length and int(content_length) != MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f90aac23680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:48:23.549369Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.52s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 2, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -467,7 +467,7 @@
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
- if len(body) > MAX_WEBHOOK_BODY_SIZE:
+ if len(body) != MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f947b9bb680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:57:25.365951Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 0, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if len(raw) < MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...........F
=================================== FAILURES ===================================
__________________ test_webhook_passes_delivery_id_to_handler __________________
test_client = <starlette.testclient.TestClient object at 0x7f954b5a9bf0>
def test_webhook_passes_delivery_id_to_handler(test_client: TestClient):
"""Test the GitHub delivery GUID reaches the handler and the log context."""
body = json.dumps({"zen": "test"}).encode()
delivery = "72d3162e-cc78-11e3-81ab-4c9367dc0958"
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": delivery,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] == delivery
E AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
tests/test_main.py:579: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:37:49.694512Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_passes_delivery_id_to_handler - AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 227 passed, 3 deselected, 1 warning in 3.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -459,7 +459,7 @@
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
- if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
+ if content_length and int(content_length) < MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fca14eab680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:42:28.169213Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 2, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -467,7 +467,7 @@
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
- if len(body) > MAX_WEBHOOK_BODY_SIZE:
+ if len(body) < MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f5c6ef73680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:56:40.549024Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 0, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if len(raw) <= MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...........F
=================================== FAILURES ===================================
__________________ test_webhook_passes_delivery_id_to_handler __________________
test_client = <starlette.testclient.TestClient object at 0x7fb1323a9bf0>
def test_webhook_passes_delivery_id_to_handler(test_client: TestClient):
"""Test the GitHub delivery GUID reaches the handler and the log context."""
body = json.dumps({"zen": "test"}).encode()
delivery = "72d3162e-cc78-11e3-81ab-4c9367dc0958"
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": delivery,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] == delivery
E AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
tests/test_main.py:579: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:49:01.660309Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_passes_delivery_id_to_handler - AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 227 passed, 3 deselected, 1 warning in 3.73s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -459,7 +459,7 @@
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
- if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
+ if content_length and int(content_length) <= MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f2a01d03680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:23:27.409075Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.52s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 2, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -467,7 +467,7 @@
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
- if len(body) > MAX_WEBHOOK_BODY_SIZE:
+ if len(body) <= MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f062b7bf680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:19:56.102210Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.59s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 0, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if len(raw) >= MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -459,7 +459,7 @@
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
- if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
+ if content_length and int(content_length) >= MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.24s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 2, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -467,7 +467,7 @@
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
- if len(body) > MAX_WEBHOOK_BODY_SIZE:
+ if len(body) >= MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 0, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if len(raw) is MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.............F
=================================== FAILURES ===================================
_ test_webhook_drops_malformed_delivery_id[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] _
test_client = <starlette.testclient.TestClient object at 0x7f1fca6b5e10>
header = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
@pytest.mark.parametrize("header", ["", "x" * 65, "not a guid!", "../../etc"])
def test_webhook_drops_malformed_delivery_id(test_client: TestClient, header: str):
"""Test a delivery header that is not a GUID is neither logged nor forwarded."""
body = json.dumps({"zen": "test"}).encode()
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": header,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] is None
E AssertionError: assert 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is None
tests/test_main.py:606: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:23:55.405941Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_drops_malformed_delivery_id[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] - AssertionError: assert 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 229 passed, 3 deselected, 1 warning in 3.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -459,7 +459,7 @@
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
- if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
+ if content_length and int(content_length) is MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...F
=================================== FAILURES ===================================
____________________ test_webhook_content_length_too_large _____________________
test_client = <starlette.testclient.TestClient object at 0x7fa139d93350>
def test_webhook_content_length_too_large(test_client: TestClient):
"""Test webhook rejects requests with Content-Length exceeding limit."""
response = test_client.post(
"/webhook",
content=b"x", # Small actual body
headers={
"Content-Type": "application/json",
"Content-Length": "20000000", # 20MB - exceeds 10MB limit
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 401 == 413
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:378: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T21:17:36.440990Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:17:36.441794Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T21:17:36.440990Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_content_length_too_large - assert 401 == 413
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 219 passed, 3 deselected, 1 warning in 3.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 2, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -467,7 +467,7 @@
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
- if len(body) > MAX_WEBHOOK_BODY_SIZE:
+ if len(body) is MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....F
=================================== FAILURES ===================================
_________________________ test_webhook_body_too_large __________________________
test_client = <starlette.testclient.TestClient object at 0x7f326a2a78a0>
def test_webhook_body_too_large(test_client: TestClient):
"""Test webhook rejects requests where actual body exceeds limit.
This tests the secondary body size check (lines 231-232) which catches
cases where Content-Length header is missing or incorrect.
"""
# Create a body larger than MAX_WEBHOOK_BODY_SIZE (10MB)
large_body = b"x" * (10 * 1024 * 1024 + 1) # 10MB + 1 byte
# Set Content-Length to a small value to bypass the header check (line 222)
# but the actual body size check (line 230) should still catch it
response = test_client.post(
"/webhook",
content=large_body,
headers={
"Content-Type": "application/json",
"Content-Length": "100", # Lie about size to bypass first check
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 401 == 413
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:403: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T22:12:06.438746Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:12:06.439651Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T22:12:06.438746Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_body_too_large - assert 401 == 413
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 220 passed, 3 deselected, 1 warning in 3.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 0, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if len(raw) is not MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...........F
=================================== FAILURES ===================================
__________________ test_webhook_passes_delivery_id_to_handler __________________
test_client = <starlette.testclient.TestClient object at 0x7f3904235bf0>
def test_webhook_passes_delivery_id_to_handler(test_client: TestClient):
"""Test the GitHub delivery GUID reaches the handler and the log context."""
body = json.dumps({"zen": "test"}).encode()
delivery = "72d3162e-cc78-11e3-81ab-4c9367dc0958"
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": delivery,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] == delivery
E AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
tests/test_main.py:579: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:30:49.580174Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_passes_delivery_id_to_handler - AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 227 passed, 3 deselected, 1 warning in 3.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -459,7 +459,7 @@
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
- if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
+ if content_length and int(content_length) is not MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f58ffc63680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T23:02:23.205417Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 2, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -467,7 +467,7 @@
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
- if len(body) > MAX_WEBHOOK_BODY_SIZE:
+ if len(body) is not MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f4e7dc8f680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:17:11.649770Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.60s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_Eq, occurrence: 0, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -219,7 +219,7 @@
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
- if match is Match.FULL:
+ if match == Match.FULL:
return route_path
if match is Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response......................................................................... [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_Eq, occurrence: 1, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if match == Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_NotEq, occurrence: 0, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -219,7 +219,7 @@
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
- if match is Match.FULL:
+ if match != Match.FULL:
return route_path
if match is Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response......................................................................... [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7fa40d596660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140342577274832'>, 'http_request_duration_seconds': ...='140342577273488'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140342577272816'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
response = test_client.post(path, content=b"request body")
assert response.status_code == 404
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == [
UNMATCHED_ENDPOINT,
UNMATCHED_ENDPOINT,
]
E AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
E
E At index 0 diff: '/openapi.json' != 'unmatched'
E
E Full diff:
E [
E - 'unmatched',
E - 'unmatched',
E + '/openapi.json',
E + '/openapi.json',
E ]
tests/test_http_metrics.py:101: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:45:13.399560Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T20:45:13.401729Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
At index 0 diff: '/openapi.json' != 'unmatched'
Full diff:
[
- 'unmatched',
- 'unmatched',
+ '/openapi.json',
+ '/openapi.json',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_NotEq, occurrence: 1, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if match != Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f44a7c7e660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='139932851888080'>, 'http_request_duration_seconds': ...='139932851886736'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='139932851886064'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
response = test_client.post(path, content=b"request body")
assert response.status_code == 404
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == [
UNMATCHED_ENDPOINT,
UNMATCHED_ENDPOINT,
]
E AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
E
E At index 0 diff: '/openapi.json' != 'unmatched'
E
E Full diff:
E [
E - 'unmatched',
E - 'unmatched',
E + '/openapi.json',
E + '/openapi.json',
E ]
tests/test_http_metrics.py:101: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:47:03.139167Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T22:47:03.141307Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
At index 0 diff: '/openapi.json' != 'unmatched'
Full diff:
[
- 'unmatched',
- 'unmatched',
+ '/openapi.json',
+ '/openapi.json',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Lt, occurrence: 0, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -219,7 +219,7 @@
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
- if match is Match.FULL:
+ if match < Match.FULL:
return route_path
if match is Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response......................................................................... [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7fa4b55e2660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140345395732432'>, 'http_request_duration_seconds': ...='140345395731088'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140345395730416'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:243: in metrics_middleware
endpoint = _metric_endpoint_label(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7fa4b57541a0>
def _metric_endpoint_label(request: Request) -> str:
"""Return a bounded Prometheus endpoint label for a request.
Route templates are defined by the application, so labels such as
``/widgets/{widget_id}`` have bounded cardinality even when the path
parameter is attacker controlled. Requests that do not match a registered
route share one fallback label instead of exposing their raw URL paths.
Args:
request: Incoming HTTP request.
Returns:
The matched route template or the unmatched-route fallback.
"""
current_route = request.scope.get("route")
current_path = getattr(current_route, "path", None)
if isinstance(current_path, str):
return current_path
partial_match: str | None = None
for route in request.app.routes:
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
> if match < Match.FULL:
^^^^^^^^^^^^^^^^^^
E TypeError: '<' not supported between instances of 'Match' and 'Match'
stampbot/main.py:222: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: '<' not supported between instances of 'Match' and 'Match'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Lt, occurrence: 1, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if match < Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f28dab96660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='139813447294928'>, 'http_request_duration_seconds': ...='139813447293584'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='139813447292912'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:243: in metrics_middleware
endpoint = _metric_endpoint_label(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f28dadc41a0>
def _metric_endpoint_label(request: Request) -> str:
"""Return a bounded Prometheus endpoint label for a request.
Route templates are defined by the application, so labels such as
``/widgets/{widget_id}`` have bounded cardinality even when the path
parameter is attacker controlled. Requests that do not match a registered
route share one fallback label instead of exposing their raw URL paths.
Args:
request: Incoming HTTP request.
Returns:
The matched route template or the unmatched-route fallback.
"""
current_route = request.scope.get("route")
current_path = getattr(current_route, "path", None)
if isinstance(current_path, str):
return current_path
partial_match: str | None = None
for route in request.app.routes:
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
if match is Match.FULL:
return route_path
> if match < Match.PARTIAL and partial_match is None:
^^^^^^^^^^^^^^^^^^^^^
E TypeError: '<' not supported between instances of 'Match' and 'Match'
stampbot/main.py:224: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: '<' not supported between instances of 'Match' and 'Match'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_LtE, occurrence: 0, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -219,7 +219,7 @@
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
- if match is Match.FULL:
+ if match <= Match.FULL:
return route_path
if match is Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response......................................................................... [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f7e48e0a660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140180368121808'>, 'http_request_duration_seconds': ...='140180368120464'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140180368119792'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:243: in metrics_middleware
endpoint = _metric_endpoint_label(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f7e490bc1a0>
def _metric_endpoint_label(request: Request) -> str:
"""Return a bounded Prometheus endpoint label for a request.
Route templates are defined by the application, so labels such as
``/widgets/{widget_id}`` have bounded cardinality even when the path
parameter is attacker controlled. Requests that do not match a registered
route share one fallback label instead of exposing their raw URL paths.
Args:
request: Incoming HTTP request.
Returns:
The matched route template or the unmatched-route fallback.
"""
current_route = request.scope.get("route")
current_path = getattr(current_route, "path", None)
if isinstance(current_path, str):
return current_path
partial_match: str | None = None
for route in request.app.routes:
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
> if match <= Match.FULL:
^^^^^^^^^^^^^^^^^^^
E TypeError: '<=' not supported between instances of 'Match' and 'Match'
stampbot/main.py:222: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: '<=' not supported between instances of 'Match' and 'Match'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_LtE, occurrence: 1, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if match <= Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7ff0f47da660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140672872325072'>, 'http_request_duration_seconds': ...='140672872323728'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140672872323056'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:243: in metrics_middleware
endpoint = _metric_endpoint_label(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7ff0f49581a0>
def _metric_endpoint_label(request: Request) -> str:
"""Return a bounded Prometheus endpoint label for a request.
Route templates are defined by the application, so labels such as
``/widgets/{widget_id}`` have bounded cardinality even when the path
parameter is attacker controlled. Requests that do not match a registered
route share one fallback label instead of exposing their raw URL paths.
Args:
request: Incoming HTTP request.
Returns:
The matched route template or the unmatched-route fallback.
"""
current_route = request.scope.get("route")
current_path = getattr(current_route, "path", None)
if isinstance(current_path, str):
return current_path
partial_match: str | None = None
for route in request.app.routes:
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
if match is Match.FULL:
return route_path
> if match <= Match.PARTIAL and partial_match is None:
^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '<=' not supported between instances of 'Match' and 'Match'
stampbot/main.py:224: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: '<=' not supported between instances of 'Match' and 'Match'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Gt, occurrence: 0, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -219,7 +219,7 @@
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
- if match is Match.FULL:
+ if match > Match.FULL:
return route_path
if match is Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response......................................................................... [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7fddec1ae660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140591127388112'>, 'http_request_duration_seconds': ...='140591127386768'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140591127386096'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:243: in metrics_middleware
endpoint = _metric_endpoint_label(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7fddec34c1a0>
def _metric_endpoint_label(request: Request) -> str:
"""Return a bounded Prometheus endpoint label for a request.
Route templates are defined by the application, so labels such as
``/widgets/{widget_id}`` have bounded cardinality even when the path
parameter is attacker controlled. Requests that do not match a registered
route share one fallback label instead of exposing their raw URL paths.
Args:
request: Incoming HTTP request.
Returns:
The matched route template or the unmatched-route fallback.
"""
current_route = request.scope.get("route")
current_path = getattr(current_route, "path", None)
if isinstance(current_path, str):
return current_path
partial_match: str | None = None
for route in request.app.routes:
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
> if match > Match.FULL:
^^^^^^^^^^^^^^^^^^
E TypeError: '>' not supported between instances of 'Match' and 'Match'
stampbot/main.py:222: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: '>' not supported between instances of 'Match' and 'Match'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Gt, occurrence: 1, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if match > Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f1cfdbae660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='139762493743056'>, 'http_request_duration_seconds': ...='139762493741712'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='139762493741040'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:243: in metrics_middleware
endpoint = _metric_endpoint_label(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f1cfdcac1a0>
def _metric_endpoint_label(request: Request) -> str:
"""Return a bounded Prometheus endpoint label for a request.
Route templates are defined by the application, so labels such as
``/widgets/{widget_id}`` have bounded cardinality even when the path
parameter is attacker controlled. Requests that do not match a registered
route share one fallback label instead of exposing their raw URL paths.
Args:
request: Incoming HTTP request.
Returns:
The matched route template or the unmatched-route fallback.
"""
current_route = request.scope.get("route")
current_path = getattr(current_route, "path", None)
if isinstance(current_path, str):
return current_path
partial_match: str | None = None
for route in request.app.routes:
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
if match is Match.FULL:
return route_path
> if match > Match.PARTIAL and partial_match is None:
^^^^^^^^^^^^^^^^^^^^^
E TypeError: '>' not supported between instances of 'Match' and 'Match'
stampbot/main.py:224: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: '>' not supported between instances of 'Match' and 'Match'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_GtE, occurrence: 0, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -219,7 +219,7 @@
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
- if match is Match.FULL:
+ if match >= Match.FULL:
return route_path
if match is Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response......................................................................... [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7fbc6aab6660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140447222554576'>, 'http_request_duration_seconds': ...='140447222553232'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140447222552560'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:243: in metrics_middleware
endpoint = _metric_endpoint_label(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7fbc6acf41a0>
def _metric_endpoint_label(request: Request) -> str:
"""Return a bounded Prometheus endpoint label for a request.
Route templates are defined by the application, so labels such as
``/widgets/{widget_id}`` have bounded cardinality even when the path
parameter is attacker controlled. Requests that do not match a registered
route share one fallback label instead of exposing their raw URL paths.
Args:
request: Incoming HTTP request.
Returns:
The matched route template or the unmatched-route fallback.
"""
current_route = request.scope.get("route")
current_path = getattr(current_route, "path", None)
if isinstance(current_path, str):
return current_path
partial_match: str | None = None
for route in request.app.routes:
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
> if match >= Match.FULL:
^^^^^^^^^^^^^^^^^^^
E TypeError: '>=' not supported between instances of 'Match' and 'Match'
stampbot/main.py:222: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: '>=' not supported between instances of 'Match' and 'Match'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_GtE, occurrence: 1, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if match >= Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f3fea39e660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='139912491737040'>, 'http_request_duration_seconds': ...='139912491735696'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='139912491735024'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:243: in metrics_middleware
endpoint = _metric_endpoint_label(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f3fea5e81a0>
def _metric_endpoint_label(request: Request) -> str:
"""Return a bounded Prometheus endpoint label for a request.
Route templates are defined by the application, so labels such as
``/widgets/{widget_id}`` have bounded cardinality even when the path
parameter is attacker controlled. Requests that do not match a registered
route share one fallback label instead of exposing their raw URL paths.
Args:
request: Incoming HTTP request.
Returns:
The matched route template or the unmatched-route fallback.
"""
current_route = request.scope.get("route")
current_path = getattr(current_route, "path", None)
if isinstance(current_path, str):
return current_path
partial_match: str | None = None
for route in request.app.routes:
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
if match is Match.FULL:
return route_path
> if match >= Match.PARTIAL and partial_match is None:
^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '>=' not supported between instances of 'Match' and 'Match'
stampbot/main.py:224: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - TypeError: '>=' not supported between instances of 'Match' and 'Match'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 0, definition_name: _setup_is_available
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -85,7 +85,7 @@
if not _setup_requested():
return False
- app_configured = is_configured() if configured is None else configured
+ app_configured = is_configured() if configured is not None else configured
return not app_configured or _setup_allowed_when_configured()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 1, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -219,7 +219,7 @@
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
- if match is Match.FULL:
+ if match is not Match.FULL:
return route_path
if match is Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response......................................................................... [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f8580e1a660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140211370663888'>, 'http_request_duration_seconds': ...='140211370662544'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140211370661872'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
response = test_client.post(path, content=b"request body")
assert response.status_code == 404
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == [
UNMATCHED_ENDPOINT,
UNMATCHED_ENDPOINT,
]
E AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
E
E At index 0 diff: '/openapi.json' != 'unmatched'
E
E Full diff:
E [
E - 'unmatched',
E - 'unmatched',
E + '/openapi.json',
E + '/openapi.json',
E ]
tests/test_http_metrics.py:101: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:03:00.012251Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T22:03:00.014594Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
At index 0 diff: '/openapi.json' != 'unmatched'
Full diff:
[
- 'unmatched',
- 'unmatched',
+ '/openapi.json',
+ '/openapi.json',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 2, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if match is not Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f83092aa660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140200771887056'>, 'http_request_duration_seconds': ...='140200771885712'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140200771885040'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
response = test_client.post(path, content=b"request body")
assert response.status_code == 404
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == [
UNMATCHED_ENDPOINT,
UNMATCHED_ENDPOINT,
]
E AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
E
E At index 0 diff: '/openapi.json' != 'unmatched'
E
E Full diff:
E [
E - 'unmatched',
E - 'unmatched',
E + '/openapi.json',
E + '/openapi.json',
E ]
tests/test_http_metrics.py:101: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:29:06.802171Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T21:29:06.804259Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
At index 0 diff: '/openapi.json' != 'unmatched'
Full diff:
[
- 'unmatched',
- 'unmatched',
+ '/openapi.json',
+ '/openapi.json',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 3, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if match is Match.PARTIAL and partial_match is not None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
....................................F
=================================== FAILURES ===================================
_____________ test_method_not_allowed_uses_matching_route_template _____________
routed_app = <fastapi.applications.FastAPI object at 0x7fbef2a73620>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140458091558768'>, 'http_request_duration_seconds': ...='140458091559440'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140458091559776'>, ...}
def test_method_not_allowed_uses_matching_route_template(
routed_app: FastAPI,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""A 405 remains attributable to the bounded matching route."""
client = TestClient(routed_app)
response = client.get("/method-specific")
assert response.status_code == 405
> http_metric_spies["http_requests_total"].labels.assert_called_once_with(
method="GET",
endpoint="/method-specific",
status=405,
)
tests/test_http_metrics.py:161:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='http_requests_total.labels' id='140458093019888'>
args = ()
kwargs = {'method': 'GET', 'endpoint': '/method-specific', 'status': 405}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: labels(method='GET', endpoint='/method-specific', status=405)
E Actual: labels(method='GET', endpoint='unmatched', status=405)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:23:44.514067Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/method-specific "HTTP/1.1 405 Method Not Allowed"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/method-specific "HTTP/1.1 405 Method Not Allowed"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_method_not_allowed_uses_matching_route_template - AssertionError: expected call not found.
Expected: labels(method='GET', endpoint='/method-specific', status=405)
Actual: labels(method='GET', endpoint='unmatched', status=405)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 180 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 0, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -172,7 +172,7 @@
try:
yield
finally:
- if metrics_runtime is not None:
+ if metrics_runtime is None:
stop_metrics_server(*metrics_runtime)
# Shutdown
logger.info("Shutting down stampbot")........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................F
=================================== FAILURES ===================================
__________________ test_lifespan_startup_shutdown_configured ___________________
def test_lifespan_startup_shutdown_configured():
"""Test app lifespan startup and shutdown when configured."""
from stampbot.main import app
# Use TestClient as context manager to trigger lifespan events
> with TestClient(app) as client:
^^^^^^^^^^^^^^^
tests/test_main.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:708: in __exit__
self.exit_stack.close()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:627: in close
self.__exit__(None, None, None)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:619: in __exit__
raise exc
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:604: in __exit__
if cb(*exc_details):
^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:482: in _exit_wrapper
callback(*args, **kwds)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:701: in wait_shutdown
portal.call(self.wait_shutdown)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:748: in wait_shutdown
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:738: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:221: in __aexit__
await anext(self.gen)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7fd6c47c6a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(
"Prometheus metrics listener started",
extra={"host": metrics_host, "port": metrics_port},
)
try:
yield
finally:
if metrics_runtime is None:
> stop_metrics_server(*metrics_runtime)
E TypeError: stampbot.metrics.stop_metrics_server() argument after * must be an iterable, not NoneType
stampbot/main.py:176: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:48:38.411885Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:48:38.412056Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "HTTP Request: GET http://testserver/health \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:48:38.413520Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:48:38.411885Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:48:38.412056Z'}
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/health "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_startup_shutdown_configured - TypeError: stampbot.metrics.stop_metrics_server() argument after * must be an iterable, not NoneType
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 197 passed, 3 deselected, 1 warning in 3.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 0, definition_name: _setup_is_available
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -82,7 +82,7 @@
def _setup_is_available(configured: bool | None = None) -> bool:
"""Return whether setup routes should be available for the current state."""
- if not _setup_requested():
+ if _setup_requested():
return False
app_configured = is_configured() if configured is None else configured........................................................................ [ 15%]
........................................................................ [ 31%]
............................................................F
=================================== FAILURES ===================================
________________ test_ready_endpoint_unconfigured_setup_enabled ________________
test_client = <starlette.testclient.TestClient object at 0x7f80b938f680>
def test_ready_endpoint_unconfigured_setup_enabled(test_client: TestClient):
"""Test readiness is 200 in setup mode so /setup stays reachable.
An unconfigured pod with setup mode on must stay in the Service endpoints,
otherwise the operator can never reach /setup to configure it.
"""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.get.side_effect = lambda key, default=None: {
"setup_enabled": True,
"setup_allow_configured": False,
}.get(key, default)
response = test_client.get("/ready")
> assert response.status_code == 200
E assert 503 == 200
E + where 503 = <Response [503 Service Unavailable]>.status_code
tests/test_main.py:123: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 503 Service Unavailable\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:56:21.346393Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 503 Service Unavailable"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_unconfigured_setup_enabled - assert 503 == 200
+ where 503 = <Response [503 Service Unavailable]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 204 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 1, definition_name: _setup_is_available
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -86,7 +86,7 @@
return False
app_configured = is_configured() if configured is None else configured
- return not app_configured or _setup_allowed_when_configured()
+ return app_configured or _setup_allowed_when_configured()
def _require_setup_access() -> None:........................................................................ [ 15%]
........................................................................ [ 31%]
............................................................F
=================================== FAILURES ===================================
________________ test_ready_endpoint_unconfigured_setup_enabled ________________
test_client = <starlette.testclient.TestClient object at 0x7f5d0e1b3680>
def test_ready_endpoint_unconfigured_setup_enabled(test_client: TestClient):
"""Test readiness is 200 in setup mode so /setup stays reachable.
An unconfigured pod with setup mode on must stay in the Service endpoints,
otherwise the operator can never reach /setup to configure it.
"""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.get.side_effect = lambda key, default=None: {
"setup_enabled": True,
"setup_allow_configured": False,
}.get(key, default)
response = test_client.get("/ready")
> assert response.status_code == 200
E assert 503 == 200
E + where 503 = <Response [503 Service Unavailable]>.status_code
tests/test_main.py:123: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 503 Service Unavailable\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:29:25.942421Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 503 Service Unavailable"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_unconfigured_setup_enabled - assert 503 == 200
+ where 503 = <Response [503 Service Unavailable]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 204 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 2, definition_name: _require_setup_access
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -95,7 +95,7 @@
Raises:
HTTPException: If setup is disabled or automatically closed.
"""
- if not _setup_requested():
+ if _setup_requested():
raise HTTPException(status_code=403, detail="Setup is disabled")
if is_configured() and not _setup_allowed_when_configured():
raise HTTPException(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...........F
=================================== FAILURES ===================================
_____ TestSetupEndpointsConfigured.test_setup_auto_closes_when_configured ______
self = <tests.test_setup_endpoints.TestSetupEndpointsConfigured object at 0x7f94c8992350>
def test_setup_auto_closes_when_configured(self):
"""Test an initial-setup opt-in does not keep setup open after configuration."""
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(
mock_settings,
enabled=True,
base_url="https://configured.example.test",
)
from fastapi.testclient import TestClient
from stampbot.main import app
response = TestClient(app, raise_server_exceptions=False).get("/setup")
assert response.status_code == 403
> assert "already configured" in response.json()["detail"].lower()
E AssertionError: assert 'already configured' in 'setup is disabled'
E + where 'setup is disabled' = <built-in method lower of str object at 0x7f94c8c7fab0>()
E + where <built-in method lower of str object at 0x7f94c8c7fab0> = 'Setup is disabled'.lower
tests/test_setup_endpoints.py:99: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 403 Forbidden\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:14:21.323542Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 403 Forbidden"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsConfigured::test_setup_auto_closes_when_configured - AssertionError: assert 'already configured' in 'setup is disabled'
+ where 'setup is disabled' = <built-in method lower of str object at 0x7f94c8c7fab0>()
+ where <built-in method lower of str object at 0x7f94c8c7fab0> = 'Setup is disabled'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 299 passed, 3 deselected, 1 warning in 3.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 3, definition_name: _require_setup_access
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -97,7 +97,7 @@
"""
if not _setup_requested():
raise HTTPException(status_code=403, detail="Setup is disabled")
- if is_configured() and not _setup_allowed_when_configured():
+ if is_configured() and _setup_allowed_when_configured():
raise HTTPException(
status_code=403,
detail="Setup is closed because Stampbot is already configured",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...........F
=================================== FAILURES ===================================
_____ TestSetupEndpointsConfigured.test_setup_auto_closes_when_configured ______
self = <tests.test_setup_endpoints.TestSetupEndpointsConfigured object at 0x7f2d5d982350>
def test_setup_auto_closes_when_configured(self):
"""Test an initial-setup opt-in does not keep setup open after configuration."""
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(
mock_settings,
enabled=True,
base_url="https://configured.example.test",
)
from fastapi.testclient import TestClient
from stampbot.main import app
response = TestClient(app, raise_server_exceptions=False).get("/setup")
> assert response.status_code == 403
E assert 200 == 403
E + where 200 = <Response [200 OK]>.status_code
tests/test_setup_endpoints.py:98: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:55:38.564792Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsConfigured::test_setup_auto_closes_when_configured - assert 200 == 403
+ where 200 = <Response [200 OK]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 299 passed, 3 deselected, 1 warning in 3.84s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 4, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -145,7 +145,7 @@
# Log setup mode status
configured = is_configured()
- if not configured:
+ if configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 5, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -157,7 +157,7 @@
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
- if not metrics_host.strip():
+ if metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7fe2302c2a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if metrics_host.strip():
> raise RuntimeError("metrics_host must not be empty")
E RuntimeError: metrics_host must not be empty
stampbot/main.py:161: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:59:36.580765Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:59:36.581000Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:59:36.580765Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:59:36.581000Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_host must not be empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 6, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f79312d2a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if 1 <= metrics_port <= 65535:
> raise RuntimeError("metrics_port must be between 1 and 65535")
E RuntimeError: metrics_port must be between 1 and 65535
stampbot/main.py:163: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:52:52.992433Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:52:52.992686Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:52:52.992433Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:52:52.992686Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must be between 1 and 65535
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 7, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -217,7 +217,7 @@
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
- if not isinstance(route_path, str):
+ if isinstance(route_path, str):
continue
if match is Match.FULL:
return route_path........................................................................ [ 15%]
........................................................................ [ 31%]
..................................F
=================================== FAILURES ===================================
____________________ test_dynamic_path_uses_route_template _____________________
routed_app = <fastapi.applications.FastAPI object at 0x7f920f278ad0>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140265296203120'>, 'http_request_duration_seconds': ...='140265296203792'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140265296202784'>, ...}
def test_dynamic_path_uses_route_template(
routed_app: FastAPI,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Path parameters are represented by their stable route template."""
client = TestClient(routed_app)
response = client.get("/widgets/user-controlled-123")
assert response.status_code == 200
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == ["/widgets/{widget_id}"]
E AssertionError: assert ['unmatched'] == ['/widgets/{widget_id}']
E
E At index 0 diff: 'unmatched' != '/widgets/{widget_id}'
E
E Full diff:
E [
E - '/widgets/{widget_id}',
E + 'unmatched',
E ]
tests/test_http_metrics.py:130: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:18:50.418885Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/widgets/user-controlled-123 "HTTP/1.1 200 OK"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/widgets/user-controlled-123 "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_dynamic_path_uses_route_template - AssertionError: assert ['unmatched'] == ['/widgets/{widget_id}']
At index 0 diff: 'unmatched' != '/widgets/{widget_id}'
Full diff:
[
- '/widgets/{widget_id}',
+ 'unmatched',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 178 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 8, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -327,7 +327,7 @@
# entry is the original client (proxies append to the right).
client_ip = raw.split(",")[0].strip()
- if not client_ip and scope.get("client"):
+ if client_ip and scope.get("client"):
client_ip = scope["client"][0]
if client_ip:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........F
=================================== FAILURES ===================================
______________ test_logging_middleware_respects_configured_header ______________
def test_logging_middleware_respects_configured_header():
"""Test that logging_middleware uses the configured client_ip_header setting."""
from unittest.mock import patch
import structlog.contextvars
from stampbot.main import app
with patch("stampbot.main.settings") as mock_settings:
mock_settings.get = lambda key, default=None: (
"X-Real-IP" if key == "client_ip_header" else default
)
client = TestClient(app)
captured: dict[str, str | None] = {}
original_bind = structlog.contextvars.bind_contextvars
def capturing_bind(**kw: object) -> None:
captured.update({k: str(v) for k, v in kw.items()})
original_bind(**kw)
with patch("stampbot.main.structlog.contextvars.bind_contextvars", capturing_bind):
client.get("/health", headers={"X-Real-IP": "198.51.100.7"})
> assert captured.get("client_ip") == "198.51.100.7"
E AssertionError: assert 'testclient' == '198.51.100.7'
E
E - 198.51.100.7
E + testclient
tests/test_main.py:481: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/health \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:52:13.194772Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/health "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_logging_middleware_respects_configured_header - AssertionError: assert 'testclient' == '198.51.100.7'
- 198.51.100.7
+ testclient
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 224 passed, 3 deselected, 1 warning in 3.56s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 9, definition_name: root
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -347,7 +347,7 @@
Redirect to /setup if unconfigured, otherwise JSON status response.
"""
configured = is_configured()
- if not configured and _setup_is_available(configured=False):
+ if configured and _setup_is_available(configured=False):
return RedirectResponse(url="/setup", status_code=307)
return JSONResponse(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...............F
=================================== FAILURES ===================================
_ TestSetupEndpointsUnconfigured.test_root_redirects_to_setup_when_unconfigured _
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7f8e56b52490>
def test_root_redirects_to_setup_when_unconfigured(self):
"""Test root redirects to /setup when not configured."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/", follow_redirects=False)
> assert response.status_code == 307
E assert 200 == 307
E + where 200 = <Response [200 OK]>.status_code
tests/test_setup_endpoints.py:187: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:23:09.906290Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_root_redirects_to_setup_when_unconfigured - assert 200 == 307
+ where 200 = <Response [200 OK]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 303 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 10, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -415,7 +415,7 @@
Returns:
The GUID, or None when the header is missing or malformed.
"""
- if not raw:
+ if raw:
return None
if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7feb234e99d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
> response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
tests/test_main.py:351:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:493: in webhook
delivery_id = _delivery_id_label(x_github_delivery)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
raw = None
def _delivery_id_label(raw: str | None) -> str | None:
"""Return a GitHub delivery GUID suitable for logs and span attributes.
GitHub sends a 36-character GUID. The check allows letters, digits, and
hyphens up to 64 characters so a future format change still correlates,
and drops anything else rather than storing it, since the value is only
useful for matching GitHub's delivery log.
Args:
raw: Value of the ``X-GitHub-Delivery`` header, if present.
Returns:
The GUID, or None when the header is missing or malformed.
"""
if raw:
return None
> if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
^^^^^^^^
E TypeError: object of type 'NoneType' has no len()
stampbot/main.py:420: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - TypeError: object of type 'NoneType' has no len()
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 11, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if len(raw) > MAX_DELIVERY_ID_LENGTH or DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...........F
=================================== FAILURES ===================================
__________________ test_webhook_passes_delivery_id_to_handler __________________
test_client = <starlette.testclient.TestClient object at 0x7f6efcbe9bf0>
def test_webhook_passes_delivery_id_to_handler(test_client: TestClient):
"""Test the GitHub delivery GUID reaches the handler and the log context."""
body = json.dumps({"zen": "test"}).encode()
delivery = "72d3162e-cc78-11e3-81ab-4c9367dc0958"
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": delivery,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] == delivery
E AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
tests/test_main.py:579: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T23:01:15.356914Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_passes_delivery_id_to_handler - AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 227 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 12, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -447,7 +447,7 @@
413 if body too large, 503 if not configured, 500 on internal error.
"""
# Check if app is configured
- if not is_configured():
+ if is_configured():
raise HTTPException(
status_code=503,
detail="Stampbot not configured. Visit /setup to complete setup.",........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fe47f9c3680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 503 == 401
E + where 503 = <Response [503 Service Unavailable]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 503 Service Unavailable\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:56:41.833943Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 503 Service Unavailable"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 503 == 401
+ where 503 = <Response [503 Service Unavailable]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.57s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 13, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -453,7 +453,7 @@
detail="Stampbot not configured. Visit /setup to complete setup.",
)
- if not x_github_event:
+ if x_github_event:
errors_total.labels(error_type="missing_event").inc()
raise HTTPException(status_code=400, detail="Missing X-GitHub-Event header")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f241f9b3680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 400 == 401
E + where 400 = <Response [400 Bad Request]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 400 Bad Request\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:00:36.351993Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 400 Bad Request"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 400 == 401
+ where 400 = <Response [400 Bad Request]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.57s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 14, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -472,7 +472,7 @@
raise HTTPException(status_code=413, detail="Request body too large")
# Verify signature
- if not webhook_handler.verify_signature(body, x_hub_signature_256):
+ if webhook_handler.verify_signature(body, x_hub_signature_256):
webhook_signature_validations_total.labels(result="invalid").inc()
errors_total.labels(error_type="signature_invalid").inc()
logger.warning("Invalid webhook signature")........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fda739cf680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 200 == 401
E + where 200 = <Response [200 OK]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:13:58.642874Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:13:58.643880Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:13:58.642874Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 200 == 401
+ where 200 = <Response [200 OK]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 0, definition_name: _setup_is_available
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -82,7 +82,7 @@
def _setup_is_available(configured: bool | None = None) -> bool:
"""Return whether setup routes should be available for the current state."""
- if not _setup_requested():
+ if not not _setup_requested():
return False
app_configured = is_configured() if configured is None else configured........................................................................ [ 15%]
........................................................................ [ 31%]
............................................................F
=================================== FAILURES ===================================
________________ test_ready_endpoint_unconfigured_setup_enabled ________________
test_client = <starlette.testclient.TestClient object at 0x7f595dbaf680>
def test_ready_endpoint_unconfigured_setup_enabled(test_client: TestClient):
"""Test readiness is 200 in setup mode so /setup stays reachable.
An unconfigured pod with setup mode on must stay in the Service endpoints,
otherwise the operator can never reach /setup to configure it.
"""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.get.side_effect = lambda key, default=None: {
"setup_enabled": True,
"setup_allow_configured": False,
}.get(key, default)
response = test_client.get("/ready")
> assert response.status_code == 200
E assert 503 == 200
E + where 503 = <Response [503 Service Unavailable]>.status_code
tests/test_main.py:123: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 503 Service Unavailable\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:17:54.453368Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 503 Service Unavailable"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_unconfigured_setup_enabled - assert 503 == 200
+ where 503 = <Response [503 Service Unavailable]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 204 passed, 3 deselected, 1 warning in 3.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 1, definition_name: _setup_is_available
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -85,7 +85,7 @@
if not _setup_requested():
return False
- app_configured = is_configured() if configured is None else configured
+ app_configured = is_configured() if not configured is None else configured
return not app_configured or _setup_allowed_when_configured()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 2, definition_name: _require_setup_access
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -95,7 +95,7 @@
Raises:
HTTPException: If setup is disabled or automatically closed.
"""
- if not _setup_requested():
+ if not not _setup_requested():
raise HTTPException(status_code=403, detail="Setup is disabled")
if is_configured() and not _setup_allowed_when_configured():
raise HTTPException(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...........F
=================================== FAILURES ===================================
_____ TestSetupEndpointsConfigured.test_setup_auto_closes_when_configured ______
self = <tests.test_setup_endpoints.TestSetupEndpointsConfigured object at 0x7f9967b56350>
def test_setup_auto_closes_when_configured(self):
"""Test an initial-setup opt-in does not keep setup open after configuration."""
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(
mock_settings,
enabled=True,
base_url="https://configured.example.test",
)
from fastapi.testclient import TestClient
from stampbot.main import app
response = TestClient(app, raise_server_exceptions=False).get("/setup")
assert response.status_code == 403
> assert "already configured" in response.json()["detail"].lower()
E AssertionError: assert 'already configured' in 'setup is disabled'
E + where 'setup is disabled' = <built-in method lower of str object at 0x7f9967c12a30>()
E + where <built-in method lower of str object at 0x7f9967c12a30> = 'Setup is disabled'.lower
tests/test_setup_endpoints.py:99: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 403 Forbidden\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:05:14.117223Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 403 Forbidden"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsConfigured::test_setup_auto_closes_when_configured - AssertionError: assert 'already configured' in 'setup is disabled'
+ where 'setup is disabled' = <built-in method lower of str object at 0x7f9967c12a30>()
+ where <built-in method lower of str object at 0x7f9967c12a30> = 'Setup is disabled'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 299 passed, 3 deselected, 1 warning in 3.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 3, definition_name: _require_setup_access
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -97,7 +97,7 @@
"""
if not _setup_requested():
raise HTTPException(status_code=403, detail="Setup is disabled")
- if is_configured() and not _setup_allowed_when_configured():
+ if not is_configured() and not _setup_allowed_when_configured():
raise HTTPException(
status_code=403,
detail="Setup is closed because Stampbot is already configured",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...........F
=================================== FAILURES ===================================
_____ TestSetupEndpointsConfigured.test_setup_auto_closes_when_configured ______
self = <tests.test_setup_endpoints.TestSetupEndpointsConfigured object at 0x7fab6aa7a350>
def test_setup_auto_closes_when_configured(self):
"""Test an initial-setup opt-in does not keep setup open after configuration."""
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(
mock_settings,
enabled=True,
base_url="https://configured.example.test",
)
from fastapi.testclient import TestClient
from stampbot.main import app
response = TestClient(app, raise_server_exceptions=False).get("/setup")
> assert response.status_code == 403
E assert 200 == 403
E + where 200 = <Response [200 OK]>.status_code
tests/test_setup_endpoints.py:98: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:30:48.214480Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsConfigured::test_setup_auto_closes_when_configured - assert 200 == 403
+ where 200 = <Response [200 OK]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 299 passed, 3 deselected, 1 warning in 3.82s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 4, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -145,7 +145,7 @@
# Log setup mode status
configured = is_configured()
- if not configured:
+ if not not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 5, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -147,7 +147,7 @@
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
- if _setup_is_available(configured=False):
+ if not _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 6, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -154,7 +154,7 @@
else:
logger.info("GitHub App credentials configured successfully")
- if settings.metrics_enabled:
+ if not settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():........................................................................ [ 15%]
........................................................................ [ 31%]
......................................................F
=================================== FAILURES ===================================
______________________ test_lifespan_startup_unconfigured ______________________
def test_lifespan_startup_unconfigured():
"""Test app lifespan startup when not configured (setup mode)."""
from stampbot.main import app
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = False
> with TestClient(app) as client:
^^^^^^^^^^^^^^^
tests/test_main.py:41:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:166: in lifespan
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/metrics.py:194: in start_metrics_server
server, thread = start_http_server(port=port, addr=host, registry=registry)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/exposition.py:258: in start_wsgi_server
TmpServer.address_family, addr = _get_best_family(addr, port)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/exposition.py:187: in _get_best_family
infos = socket.getaddrinfo(address, port, type=socket.SOCK_STREAM, flags=socket.AI_PASSIVE)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
host = "<MagicMock name='settings.metrics_host' id='140092426925264'>", port = 1
family = 0, type = <SocketKind.SOCK_STREAM: 1>, proto = 0
flags = <AddressInfo.AI_PASSIVE: 1>
def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
"""Resolve host and port into list of address info entries.
Translate the host/port argument into a sequence of 5-tuples that contain
all the necessary arguments for creating a socket connected to that service.
host is a domain name, a string representation of an IPv4/v6 address or
None. port is a string service name such as 'http', a numeric port number or
None. By passing None as the value of host and port, you can pass NULL to
the underlying C API.
The family, type and proto arguments can be optionally specified in order to
narrow the list of addresses returned. Passing zero as a value for each of
these arguments selects the full range of results.
"""
# We override this function since we want to translate the numeric family
# and socket type values to enum constants.
addrlist = []
> for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E socket.gaierror: [Errno -2] Name or service not known
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/socket.py:989: gaierror
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:40:49.168753Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials not configured.", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T20:40:49.168959Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "Setup is enabled; visit /setup to create your GitHub App", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:40:49.169394Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:40:49.168753Z'}
WARNING stampbot.main:main.py:149 {'event': 'GitHub App credentials not configured.', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T20:40:49.168959Z'}
INFO stampbot.main:main.py:151 {'event': 'Setup is enabled; visit /setup to create your GitHub App', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:40:49.169394Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_startup_unconfigured - socket.gaierror: [Errno -2] Name or service not known
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 198 passed, 3 deselected, 1 warning in 4.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 7, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -157,7 +157,7 @@
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
- if not metrics_host.strip():
+ if not not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7fc29dedea50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not not metrics_host.strip():
> raise RuntimeError("metrics_host must not be empty")
E RuntimeError: metrics_host must not be empty
stampbot/main.py:161: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:21:56.501421Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:21:56.501671Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:21:56.501421Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:21:56.501671Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_host must not be empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 8, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7fd89d4baa50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not not 1 <= metrics_port <= 65535:
> raise RuntimeError("metrics_port must be between 1 and 65535")
E RuntimeError: metrics_port must be between 1 and 65535
stampbot/main.py:163: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:21:22.455883Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T21:21:22.456119Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:21:22.455883Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T21:21:22.456119Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must be between 1 and 65535
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 9, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -161,7 +161,7 @@
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
- if metrics_port == int(settings.port):
+ if not metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(........................................................................ [ 15%]
........................................................................ [ 31%]
................................................................F
=================================== FAILURES ===================================
_______________ test_lifespan_starts_and_stops_metrics_listener ________________
def test_lifespan_starts_and_stops_metrics_listener():
"""Test that metrics_enabled controls the dedicated listener lifecycle."""
from stampbot.main import app
server = MagicMock()
thread = MagicMock()
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
patch("stampbot.main.start_metrics_server", return_value=(server, thread)) as start,
patch("stampbot.main.stop_metrics_server") as stop,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = "127.0.0.1"
mock_settings.metrics_port = 9090
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:191:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7fa485ed6a50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if not metrics_port == int(settings.port):
> raise RuntimeError("metrics_port must differ from the public HTTP port")
E RuntimeError: metrics_port must differ from the public HTTP port
stampbot/main.py:165: RuntimeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:38:08.429063Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:38:08.429307Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:38:08.429063Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:38:08.429307Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_starts_and_stops_metrics_listener - RuntimeError: metrics_port must differ from the public HTTP port
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 208 passed, 3 deselected, 1 warning in 3.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 10, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -172,7 +172,7 @@
try:
yield
finally:
- if metrics_runtime is not None:
+ if not metrics_runtime is not None:
stop_metrics_server(*metrics_runtime)
# Shutdown
logger.info("Shutting down stampbot")........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................F
=================================== FAILURES ===================================
__________________ test_lifespan_startup_shutdown_configured ___________________
def test_lifespan_startup_shutdown_configured():
"""Test app lifespan startup and shutdown when configured."""
from stampbot.main import app
# Use TestClient as context manager to trigger lifespan events
> with TestClient(app) as client:
^^^^^^^^^^^^^^^
tests/test_main.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:708: in __exit__
self.exit_stack.close()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:627: in close
self.__exit__(None, None, None)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:619: in __exit__
raise exc
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:604: in __exit__
if cb(*exc_details):
^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:482: in _exit_wrapper
callback(*args, **kwds)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:701: in wait_shutdown
portal.call(self.wait_shutdown)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:748: in wait_shutdown
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:738: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:221: in __aexit__
await anext(self.gen)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <fastapi.applications.FastAPI object at 0x7f2f169eaa50>
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
Args:
app: FastAPI application instance.
Yields:
None after startup, resumes for shutdown.
"""
metrics_runtime: tuple[Any, Any] | None = None
# Startup
logger.info(
f"Starting {settings.app_name} on {settings.host}:{settings.port}",
extra={
"host": settings.host,
"port": settings.port,
"log_level": settings.log_level,
},
)
# Log setup mode status
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
if _setup_is_available(configured=False):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")
else:
logger.info("GitHub App credentials configured successfully")
if settings.metrics_enabled:
metrics_host = str(settings.metrics_host)
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
if not 1 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
logger.info(
"Prometheus metrics listener started",
extra={"host": metrics_host, "port": metrics_port},
)
try:
yield
finally:
if not metrics_runtime is not None:
> stop_metrics_server(*metrics_runtime)
E TypeError: stampbot.metrics.stop_metrics_server() argument after * must be an iterable, not NoneType
stampbot/main.py:176: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:40:56.400645Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:40:56.400809Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "HTTP Request: GET http://testserver/health \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:40:56.402247Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:40:56.400645Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:40:56.400809Z'}
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/health "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_startup_shutdown_configured - TypeError: stampbot.metrics.stop_metrics_server() argument after * must be an iterable, not NoneType
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 197 passed, 3 deselected, 1 warning in 3.25s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 11, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -209,7 +209,7 @@
"""
current_route = request.scope.get("route")
current_path = getattr(current_route, "path", None)
- if isinstance(current_path, str):
+ if not isinstance(current_path, str):
return current_path
partial_match: str | None = None........................................................................ [ 15%]
........................................................................ [ 31%]
..............................F
=================================== FAILURES ===================================
_________________ test_existing_scope_route_uses_its_template __________________
def test_existing_scope_route_uses_its_template() -> None:
"""A route resolved by an outer router is used without rematching."""
request = Request(
{
"type": "http",
"route": SimpleNamespace(path="/mounted/{resource_id}"),
}
)
> assert _metric_endpoint_label(request) == "/mounted/{resource_id}"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:69:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/main.py:216: in _metric_endpoint_label
for route in request.app.routes:
^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <starlette.requests.Request object at 0x7f4bd3be7cb0>
@property
def app(self) -> Any:
> return self.scope["app"]
^^^^^^^^^^^^^^^^^
E KeyError: 'app'
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:107: KeyError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_existing_scope_route_uses_its_template - KeyError: 'app'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 174 passed, 3 deselected, 1 warning in 3.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 12, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -217,7 +217,7 @@
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
- if not isinstance(route_path, str):
+ if not not isinstance(route_path, str):
continue
if match is Match.FULL:
return route_path........................................................................ [ 15%]
........................................................................ [ 31%]
..................................F
=================================== FAILURES ===================================
____________________ test_dynamic_path_uses_route_template _____________________
routed_app = <fastapi.applications.FastAPI object at 0x7f7f17138ad0>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140183824731504'>, 'http_request_duration_seconds': ...='140183824732176'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140183824731168'>, ...}
def test_dynamic_path_uses_route_template(
routed_app: FastAPI,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Path parameters are represented by their stable route template."""
client = TestClient(routed_app)
response = client.get("/widgets/user-controlled-123")
assert response.status_code == 200
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == ["/widgets/{widget_id}"]
E AssertionError: assert ['unmatched'] == ['/widgets/{widget_id}']
E
E At index 0 diff: 'unmatched' != '/widgets/{widget_id}'
E
E Full diff:
E [
E - '/widgets/{widget_id}',
E + 'unmatched',
E ]
tests/test_http_metrics.py:130: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:19:13.584576Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/widgets/user-controlled-123 "HTTP/1.1 200 OK"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/widgets/user-controlled-123 "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_dynamic_path_uses_route_template - AssertionError: assert ['unmatched'] == ['/widgets/{widget_id}']
At index 0 diff: 'unmatched' != '/widgets/{widget_id}'
Full diff:
[
- '/widgets/{widget_id}',
+ 'unmatched',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 178 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 13, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -219,7 +219,7 @@
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
continue
- if match is Match.FULL:
+ if not match is Match.FULL:
return route_path
if match is Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response......................................................................... [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7ff3285b6660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140682332561360'>, 'http_request_duration_seconds': ...='140682332560016'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140682332559344'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
response = test_client.post(path, content=b"request body")
assert response.status_code == 404
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == [
UNMATCHED_ENDPOINT,
UNMATCHED_ENDPOINT,
]
E AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
E
E At index 0 diff: '/openapi.json' != 'unmatched'
E
E Full diff:
E [
E - 'unmatched',
E - 'unmatched',
E + '/openapi.json',
E + '/openapi.json',
E ]
tests/test_http_metrics.py:101: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:30:33.179438Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T20:30:33.181672Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
At index 0 diff: '/openapi.json' != 'unmatched'
Full diff:
[
- 'unmatched',
- 'unmatched',
+ '/openapi.json',
+ '/openapi.json',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 14, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if not match is Match.PARTIAL and partial_match is None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7ff53690a660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140691162144720'>, 'http_request_duration_seconds': ...='140691162143376'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140691162142704'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
response = test_client.post(path, content=b"request body")
assert response.status_code == 404
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == [
UNMATCHED_ENDPOINT,
UNMATCHED_ENDPOINT,
]
E AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
E
E At index 0 diff: '/openapi.json' != 'unmatched'
E
E Full diff:
E [
E - 'unmatched',
E - 'unmatched',
E + '/openapi.json',
E + '/openapi.json',
E ]
tests/test_http_metrics.py:101: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:51:36.270040Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T20:51:36.272109Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
At index 0 diff: '/openapi.json' != 'unmatched'
Full diff:
[
- 'unmatched',
- 'unmatched',
+ '/openapi.json',
+ '/openapi.json',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 15, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -266,7 +266,7 @@
# Track request size (from Content-Length header if available)
content_length = request.headers.get("content-length")
- if content_length:
+ if not content_length:
http_request_size_bytes.labels(
method=method,
endpoint=endpoint,........................................................................ [ 15%]
........................................................................ [ 31%]
.................................F
=================================== FAILURES ===================================
___________ test_exported_metrics_never_contain_an_unknown_raw_path ____________
test_client = <starlette.testclient.TestClient object at 0x7fb1b8301950>
def test_exported_metrics_never_contain_an_unknown_raw_path(test_client: TestClient) -> None:
"""The metrics registry exports only the bounded fallback for a 404."""
raw_path = "/unknown/security-regression-canary-8f3810"
> assert test_client.get(raw_path).status_code == 404
^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:113:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:482: in get
return super().get(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1053: in get
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7fb1b8303390>
call_next = <function BaseHTTPMiddleware.__call__.<locals>.call_next at 0x7fb1b3e5eb90>
@app.middleware("http")
async def metrics_middleware(request: Request, call_next: Any) -> Response:
"""Middleware to track HTTP metrics.
Args:
request: Incoming HTTP request.
call_next: Next middleware or route handler.
Returns:
HTTP response from downstream handler.
"""
method = request.method
endpoint = _metric_endpoint_label(request)
# Track in-progress requests
http_requests_in_progress.labels(method=method, endpoint=endpoint).inc()
start_time = time.time()
try:
response = await call_next(request)
duration = time.time() - start_time
# Track request metrics
http_requests_total.labels(
method=method,
endpoint=endpoint,
status=response.status_code,
).inc()
http_request_duration_seconds.labels(
method=method,
endpoint=endpoint,
).observe(duration)
# Track request size (from Content-Length header if available)
content_length = request.headers.get("content-length")
if not content_length:
http_request_size_bytes.labels(
method=method,
endpoint=endpoint,
> ).observe(int(content_length))
^^^^^^^^^^^^^^^^^^^
E TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
stampbot/main.py:273: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_exported_metrics_never_contain_an_unknown_raw_path - TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 177 passed, 3 deselected, 1 warning in 3.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 16, definition_name: metrics_middleware
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -274,7 +274,7 @@
# Track response size
response_size = response.headers.get("content-length")
- if response_size:
+ if not response_size:
http_response_size_bytes.labels(
method=method,
endpoint=endpoint,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.....F
=================================== FAILURES ===================================
_____________________ test_response_without_content_length _____________________
def test_response_without_content_length():
"""Test that responses without Content-Length header are handled correctly.
This tests the branch where response_size is None (line 142->148).
StreamingResponse doesn't include Content-Length header.
"""
from collections.abc import AsyncIterator
from fastapi.responses import StreamingResponse
from stampbot.main import app
# Track if our streaming endpoint was added
streaming_route_path = "/_test_streaming_no_cl"
async def generate() -> AsyncIterator[bytes]:
yield b"chunk1"
yield b"chunk2"
# Add a temporary streaming endpoint
@app.get(streaming_route_path)
async def streaming_no_content_length() -> StreamingResponse:
return StreamingResponse(generate(), media_type="text/plain")
client = TestClient(app)
> response = client.get(streaming_route_path)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_main.py:432:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:482: in get
return super().get(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1053: in get
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.middleware.base._CachedRequest object at 0x7f8aeb2f5db0>
call_next = <function BaseHTTPMiddleware.__call__.<locals>.call_next at 0x7f8aeb275b10>
@app.middleware("http")
async def metrics_middleware(request: Request, call_next: Any) -> Response:
"""Middleware to track HTTP metrics.
Args:
request: Incoming HTTP request.
call_next: Next middleware or route handler.
Returns:
HTTP response from downstream handler.
"""
method = request.method
endpoint = _metric_endpoint_label(request)
# Track in-progress requests
http_requests_in_progress.labels(method=method, endpoint=endpoint).inc()
start_time = time.time()
try:
response = await call_next(request)
duration = time.time() - start_time
# Track request metrics
http_requests_total.labels(
method=method,
endpoint=endpoint,
status=response.status_code,
).inc()
http_request_duration_seconds.labels(
method=method,
endpoint=endpoint,
).observe(duration)
# Track request size (from Content-Length header if available)
content_length = request.headers.get("content-length")
if content_length:
http_request_size_bytes.labels(
method=method,
endpoint=endpoint,
).observe(int(content_length))
# Track response size
response_size = response.headers.get("content-length")
if not response_size:
http_response_size_bytes.labels(
method=method,
endpoint=endpoint,
> ).observe(int(response_size))
^^^^^^^^^^^^^^^^^^
E TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
stampbot/main.py:281: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_response_without_content_length - TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 221 passed, 3 deselected, 1 warning in 3.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 17, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -314,7 +314,7 @@
receive: ASGI receive callable.
send: ASGI send callable.
"""
- if scope["type"] == "http":
+ if not scope["type"] == "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................F
=================================== FAILURES ===================================
__________________ test_lifespan_startup_shutdown_configured ___________________
def test_lifespan_startup_shutdown_configured():
"""Test app lifespan startup and shutdown when configured."""
from stampbot.main import app
# Use TestClient as context manager to trigger lifespan events
> with TestClient(app) as client:
^^^^^^^^^^^^^^^
tests/test_main.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:726: in wait_startup
message = await receive()
^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:324: in __call__
raw = Request(scope).headers.get(client_ip_header)
^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:218: in __init__
super().__init__(scope)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <starlette.requests.Request object at 0x7f4a053e6990>
scope = {'type': 'lifespan', 'state': {}, 'app': <fastapi.applications.FastAPI object at 0x7f4a056d2a50>}
receive = None
def __init__(self, scope: Scope, receive: Receive | None = None) -> None:
> assert scope["type"] in ("http", "websocket")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:87: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_startup_shutdown_configured - AssertionError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 197 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 18, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -320,7 +320,7 @@
client_ip: str | None = None
client_ip_header: str = settings.get("client_ip_header", "X-Forwarded-For")
- if client_ip_header:
+ if not client_ip_header:
raw = Request(scope).headers.get(client_ip_header)
if raw:
# X-Forwarded-For may be a comma-separated list; the leftmost........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........F
=================================== FAILURES ===================================
______________ test_logging_middleware_respects_configured_header ______________
def test_logging_middleware_respects_configured_header():
"""Test that logging_middleware uses the configured client_ip_header setting."""
from unittest.mock import patch
import structlog.contextvars
from stampbot.main import app
with patch("stampbot.main.settings") as mock_settings:
mock_settings.get = lambda key, default=None: (
"X-Real-IP" if key == "client_ip_header" else default
)
client = TestClient(app)
captured: dict[str, str | None] = {}
original_bind = structlog.contextvars.bind_contextvars
def capturing_bind(**kw: object) -> None:
captured.update({k: str(v) for k, v in kw.items()})
original_bind(**kw)
with patch("stampbot.main.structlog.contextvars.bind_contextvars", capturing_bind):
client.get("/health", headers={"X-Real-IP": "198.51.100.7"})
> assert captured.get("client_ip") == "198.51.100.7"
E AssertionError: assert 'testclient' == '198.51.100.7'
E
E - 198.51.100.7
E + testclient
tests/test_main.py:481: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/health \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:00:30.020746Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/health "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_logging_middleware_respects_configured_header - AssertionError: assert 'testclient' == '198.51.100.7'
- 198.51.100.7
+ testclient
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 224 passed, 3 deselected, 1 warning in 3.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 19, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -322,7 +322,7 @@
if client_ip_header:
raw = Request(scope).headers.get(client_ip_header)
- if raw:
+ if not raw:
# X-Forwarded-For may be a comma-separated list; the leftmost
# entry is the original client (proxies append to the right).
client_ip = raw.split(",")[0].strip()........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f747afee660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140138257031120'>, 'http_request_duration_seconds': ...='140138257029776'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140138257029104'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
> response = test_client.post(path, content=b"request body")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_http_metrics.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.main.LoggingMiddleware object at 0x7f747b078440>
scope = {'type': 'http', 'http_version': '1.1', 'method': 'POST', 'path': '/not-a-route/attacker-value-one', ...}
receive = <function _TestClientTransport.handle_request.<locals>.receive at 0x7f747b041380>
send = <function ServerErrorMiddleware.__call__.<locals>._send at 0x7f747b0421f0>
async def __call__(self, scope: Any, receive: Any, send: Any) -> None:
"""Process an ASGI event, binding client IP to the structlog context.
For HTTP scopes the middleware clears any inherited structlog context,
extracts the real client IP from the configured forwarding header (or
the direct connection address as a fallback), and binds it so that
every log record emitted during the request includes ``client_ip``.
Non-HTTP scopes (lifespan, WebSocket) are passed through unchanged.
Args:
scope: ASGI connection scope.
receive: ASGI receive callable.
send: ASGI send callable.
"""
if scope["type"] == "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None
client_ip_header: str = settings.get("client_ip_header", "X-Forwarded-For")
if client_ip_header:
raw = Request(scope).headers.get(client_ip_header)
if not raw:
# X-Forwarded-For may be a comma-separated list; the leftmost
# entry is the original client (proxies append to the right).
> client_ip = raw.split(",")[0].strip()
^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'split'
stampbot/main.py:328: AttributeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - AttributeError: 'NoneType' object has no attribute 'split'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 3.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 20, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -327,7 +327,7 @@
# entry is the original client (proxies append to the right).
client_ip = raw.split(",")[0].strip()
- if not client_ip and scope.get("client"):
+ if not not client_ip and scope.get("client"):
client_ip = scope["client"][0]
if client_ip:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........F
=================================== FAILURES ===================================
______________ test_logging_middleware_respects_configured_header ______________
def test_logging_middleware_respects_configured_header():
"""Test that logging_middleware uses the configured client_ip_header setting."""
from unittest.mock import patch
import structlog.contextvars
from stampbot.main import app
with patch("stampbot.main.settings") as mock_settings:
mock_settings.get = lambda key, default=None: (
"X-Real-IP" if key == "client_ip_header" else default
)
client = TestClient(app)
captured: dict[str, str | None] = {}
original_bind = structlog.contextvars.bind_contextvars
def capturing_bind(**kw: object) -> None:
captured.update({k: str(v) for k, v in kw.items()})
original_bind(**kw)
with patch("stampbot.main.structlog.contextvars.bind_contextvars", capturing_bind):
client.get("/health", headers={"X-Real-IP": "198.51.100.7"})
> assert captured.get("client_ip") == "198.51.100.7"
E AssertionError: assert 'testclient' == '198.51.100.7'
E
E - 198.51.100.7
E + testclient
tests/test_main.py:481: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/health \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:34:30.847923Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/health "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_logging_middleware_respects_configured_header - AssertionError: assert 'testclient' == '198.51.100.7'
- 198.51.100.7
+ testclient
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 224 passed, 3 deselected, 1 warning in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 21, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -330,7 +330,7 @@
if not client_ip and scope.get("client"):
client_ip = scope["client"][0]
- if client_ip:
+ if not client_ip:
structlog.contextvars.bind_contextvars(client_ip=client_ip)
await self.app(scope, receive, send)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........F
=================================== FAILURES ===================================
______________ test_logging_middleware_respects_configured_header ______________
def test_logging_middleware_respects_configured_header():
"""Test that logging_middleware uses the configured client_ip_header setting."""
from unittest.mock import patch
import structlog.contextvars
from stampbot.main import app
with patch("stampbot.main.settings") as mock_settings:
mock_settings.get = lambda key, default=None: (
"X-Real-IP" if key == "client_ip_header" else default
)
client = TestClient(app)
captured: dict[str, str | None] = {}
original_bind = structlog.contextvars.bind_contextvars
def capturing_bind(**kw: object) -> None:
captured.update({k: str(v) for k, v in kw.items()})
original_bind(**kw)
with patch("stampbot.main.structlog.contextvars.bind_contextvars", capturing_bind):
client.get("/health", headers={"X-Real-IP": "198.51.100.7"})
> assert captured.get("client_ip") == "198.51.100.7"
E AssertionError: assert None == '198.51.100.7'
E + where None = <built-in method get of dict object at 0x7f972cb98980>('client_ip')
E + where <built-in method get of dict object at 0x7f972cb98980> = {}.get
tests/test_main.py:481: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/health \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:47:45.559327Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/health "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_logging_middleware_respects_configured_header - AssertionError: assert None == '198.51.100.7'
+ where None = <built-in method get of dict object at 0x7f972cb98980>('client_ip')
+ where <built-in method get of dict object at 0x7f972cb98980> = {}.get
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 224 passed, 3 deselected, 1 warning in 3.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 22, definition_name: root
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -347,7 +347,7 @@
Redirect to /setup if unconfigured, otherwise JSON status response.
"""
configured = is_configured()
- if not configured and _setup_is_available(configured=False):
+ if not not configured and _setup_is_available(configured=False):
return RedirectResponse(url="/setup", status_code=307)
return JSONResponse(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...............F
=================================== FAILURES ===================================
_ TestSetupEndpointsUnconfigured.test_root_redirects_to_setup_when_unconfigured _
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7ff6f8676490>
def test_root_redirects_to_setup_when_unconfigured(self):
"""Test root redirects to /setup when not configured."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/", follow_redirects=False)
> assert response.status_code == 307
E assert 200 == 307
E + where 200 = <Response [200 OK]>.status_code
tests/test_setup_endpoints.py:187: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:32:00.934097Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_root_redirects_to_setup_when_unconfigured - assert 200 == 307
+ where 200 = <Response [200 OK]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 303 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 23, definition_name: ready
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -396,7 +396,7 @@
checks = {"configured": configured, "setup_enabled": setup_enabled}
is_ready = configured or setup_enabled
return JSONResponse(
- status_code=200 if is_ready else 503,
+ status_code=200 if not is_ready else 503,
content={"status": "ready" if is_ready else "not ready", "checks": checks},
)
........................................................................ [ 15%]
........................................................................ [ 31%]
...........................................................F
=================================== FAILURES ===================================
________________________ test_ready_endpoint_configured ________________________
test_client = <starlette.testclient.TestClient object at 0x7ff5bc19f350>
def test_ready_endpoint_configured(test_client: TestClient):
"""Test readiness endpoint returns 200 when the app is configured."""
with patch("stampbot.main.is_configured", return_value=True):
response = test_client.get("/ready")
> assert response.status_code == 200
E assert 503 == 200
E + where 503 = <Response [503 Service Unavailable]>.status_code
tests/test_main.py:102: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 503 Service Unavailable\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:23:21.345006Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 503 Service Unavailable"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_configured - assert 503 == 200
+ where 503 = <Response [503 Service Unavailable]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 203 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 24, definition_name: ready
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -397,7 +397,7 @@
is_ready = configured or setup_enabled
return JSONResponse(
status_code=200 if is_ready else 503,
- content={"status": "ready" if is_ready else "not ready", "checks": checks},
+ content={"status": "ready" if not is_ready else "not ready", "checks": checks},
)
........................................................................ [ 15%]
........................................................................ [ 31%]
...........................................................F
=================================== FAILURES ===================================
________________________ test_ready_endpoint_configured ________________________
test_client = <starlette.testclient.TestClient object at 0x7f47e49af350>
def test_ready_endpoint_configured(test_client: TestClient):
"""Test readiness endpoint returns 200 when the app is configured."""
with patch("stampbot.main.is_configured", return_value=True):
response = test_client.get("/ready")
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ready"
E AssertionError: assert 'not ready' == 'ready'
E
E - ready
E + not ready
tests/test_main.py:104: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:58:53.490403Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_configured - AssertionError: assert 'not ready' == 'ready'
- ready
+ not ready
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 203 passed, 3 deselected, 1 warning in 3.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 25, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -415,7 +415,7 @@
Returns:
The GUID, or None when the header is missing or malformed.
"""
- if not raw:
+ if not not raw:
return None
if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f7d0e3e99d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
> response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
tests/test_main.py:351:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:493: in webhook
delivery_id = _delivery_id_label(x_github_delivery)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
raw = None
def _delivery_id_label(raw: str | None) -> str | None:
"""Return a GitHub delivery GUID suitable for logs and span attributes.
GitHub sends a 36-character GUID. The check allows letters, digits, and
hyphens up to 64 characters so a future format change still correlates,
and drops anything else rather than storing it, since the value is only
useful for matching GitHub's delivery log.
Args:
raw: Value of the ``X-GitHub-Delivery`` header, if present.
Returns:
The GUID, or None when the header is missing or malformed.
"""
if not not raw:
return None
> if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
^^^^^^^^
E TypeError: object of type 'NoneType' has no len()
stampbot/main.py:420: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - TypeError: object of type 'NoneType' has no len()
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 4.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 26, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if not len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...........F
=================================== FAILURES ===================================
__________________ test_webhook_passes_delivery_id_to_handler __________________
test_client = <starlette.testclient.TestClient object at 0x7f649de72140>
def test_webhook_passes_delivery_id_to_handler(test_client: TestClient):
"""Test the GitHub delivery GUID reaches the handler and the log context."""
body = json.dumps({"zen": "test"}).encode()
delivery = "72d3162e-cc78-11e3-81ab-4c9367dc0958"
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": delivery,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] == delivery
E AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
tests/test_main.py:579: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:14:32.412690Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_passes_delivery_id_to_handler - AssertionError: assert None == '72d3162e-cc78-11e3-81ab-4c9367dc0958'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 227 passed, 3 deselected, 1 warning in 3.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 27, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -447,7 +447,7 @@
413 if body too large, 503 if not configured, 500 on internal error.
"""
# Check if app is configured
- if not is_configured():
+ if not not is_configured():
raise HTTPException(
status_code=503,
detail="Stampbot not configured. Visit /setup to complete setup.",........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f0ba48bf680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 503 == 401
E + where 503 = <Response [503 Service Unavailable]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 503 Service Unavailable\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:16:48.062828Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 503 Service Unavailable"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 503 == 401
+ where 503 = <Response [503 Service Unavailable]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.54s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 28, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -453,7 +453,7 @@
detail="Stampbot not configured. Visit /setup to complete setup.",
)
- if not x_github_event:
+ if not not x_github_event:
errors_total.labels(error_type="missing_event").inc()
raise HTTPException(status_code=400, detail="Missing X-GitHub-Event header")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f13032c7680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 400 == 401
E + where 400 = <Response [400 Bad Request]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 400 Bad Request\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:56:58.987701Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 400 Bad Request"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 400 == 401
+ where 400 = <Response [400 Bad Request]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.56s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 29, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -459,7 +459,7 @@
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
- if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
+ if not content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...F
=================================== FAILURES ===================================
____________________ test_webhook_content_length_too_large _____________________
test_client = <starlette.testclient.TestClient object at 0x7fa318ad7680>
def test_webhook_content_length_too_large(test_client: TestClient):
"""Test webhook rejects requests with Content-Length exceeding limit."""
response = test_client.post(
"/webhook",
content=b"x", # Small actual body
headers={
"Content-Type": "application/json",
"Content-Length": "20000000", # 20MB - exceeds 10MB limit
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 401 == 413
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:378: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T21:58:57.207261Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:58:57.208079Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T21:58:57.207261Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_content_length_too_large - assert 401 == 413
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 219 passed, 3 deselected, 1 warning in 3.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 30, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -467,7 +467,7 @@
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
- if len(body) > MAX_WEBHOOK_BODY_SIZE:
+ if not len(body) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f3328697680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:03:47.237048Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 31, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -472,7 +472,7 @@
raise HTTPException(status_code=413, detail="Request body too large")
# Verify signature
- if not webhook_handler.verify_signature(body, x_hub_signature_256):
+ if not not webhook_handler.verify_signature(body, x_hub_signature_256):
webhook_signature_validations_total.labels(result="invalid").inc()
errors_total.labels(error_type="signature_invalid").inc()
logger.warning("Invalid webhook signature")........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f27e6fbb680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 200 == 401
E + where 200 = <Response [200 OK]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:25:04.120625Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:25:04.121624Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:25:04.120625Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 200 == 401
+ where 200 = <Response [200 OK]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 32, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -491,7 +491,7 @@
# The header is only trusted once the HMAC check has passed. Bound length
# keeps a malformed value out of log storage.
delivery_id = _delivery_id_label(x_github_delivery)
- if delivery_id:
+ if not delivery_id:
structlog.contextvars.bind_contextvars(delivery_id=delivery_id)
# Handle event with timing........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...........F
=================================== FAILURES ===================================
__________________ test_webhook_passes_delivery_id_to_handler __________________
test_client = <starlette.testclient.TestClient object at 0x7fde807f5bf0>
def test_webhook_passes_delivery_id_to_handler(test_client: TestClient):
"""Test the GitHub delivery GUID reaches the handler and the log context."""
body = json.dumps({"zen": "test"}).encode()
delivery = "72d3162e-cc78-11e3-81ab-4c9367dc0958"
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": delivery,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
assert response.json()["delivery"] == delivery
> assert seen_context["delivery_id"] == delivery
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E KeyError: 'delivery_id'
tests/test_main.py:580: KeyError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:36:33.769377Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_passes_delivery_id_to_handler - KeyError: 'delivery_id'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 227 passed, 3 deselected, 1 warning in 3.71s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 0, definition_name: setup_page
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -540,7 +540,7 @@
manifest = create_manifest(redirect_url, webhook_url=webhook_url)
# HTML-escape the JSON for safe embedding in the form
manifest_json = html.escape(json.dumps(manifest))
- webhook_url_escaped = html.escape(webhook_url, quote=True)
+ webhook_url_escaped = html.escape(webhook_url, quote=False)
html_content = f"""
<!DOCTYPE html>........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 1, definition_name: setup_callback
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -626,7 +626,7 @@
# 3. Users can immediately rotate the webhook secret if concerned
# 4. The alternative (file download) adds friction without meaningful security gain
# since the credentials must be transmitted to the browser regardless
- private_key_escaped = html.escape(str(credentials["pem"]).replace("\n", "\\n"), quote=True)
+ private_key_escaped = html.escape(str(credentials["pem"]).replace("\n", "\\n"), quote=False)
app_id_escaped = html.escape(str(credentials["id"]), quote=True)
webhook_secret_escaped = html.escape(str(credentials["webhook_secret"]), quote=True)
app_name_escaped = html.escape(str(credentials.get("name", "Stampbot")), quote=True)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 2, definition_name: setup_callback
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -627,7 +627,7 @@
# 4. The alternative (file download) adds friction without meaningful security gain
# since the credentials must be transmitted to the browser regardless
private_key_escaped = html.escape(str(credentials["pem"]).replace("\n", "\\n"), quote=True)
- app_id_escaped = html.escape(str(credentials["id"]), quote=True)
+ app_id_escaped = html.escape(str(credentials["id"]), quote=False)
webhook_secret_escaped = html.escape(str(credentials["webhook_secret"]), quote=True)
app_name_escaped = html.escape(str(credentials.get("name", "Stampbot")), quote=True)
app_slug = urllib.parse.quote(str(credentials.get("slug", "stampbot")), safe="")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 3, definition_name: setup_callback
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -628,7 +628,7 @@
# since the credentials must be transmitted to the browser regardless
private_key_escaped = html.escape(str(credentials["pem"]).replace("\n", "\\n"), quote=True)
app_id_escaped = html.escape(str(credentials["id"]), quote=True)
- webhook_secret_escaped = html.escape(str(credentials["webhook_secret"]), quote=True)
+ webhook_secret_escaped = html.escape(str(credentials["webhook_secret"]), quote=False)
app_name_escaped = html.escape(str(credentials.get("name", "Stampbot")), quote=True)
app_slug = urllib.parse.quote(str(credentials.get("slug", "stampbot")), safe="")
installation_url = html.escape(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 4, definition_name: setup_callback
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -629,7 +629,7 @@
private_key_escaped = html.escape(str(credentials["pem"]).replace("\n", "\\n"), quote=True)
app_id_escaped = html.escape(str(credentials["id"]), quote=True)
webhook_secret_escaped = html.escape(str(credentials["webhook_secret"]), quote=True)
- app_name_escaped = html.escape(str(credentials.get("name", "Stampbot")), quote=True)
+ app_name_escaped = html.escape(str(credentials.get("name", "Stampbot")), quote=False)
app_slug = urllib.parse.quote(str(credentials.get("slug", "stampbot")), safe="")
installation_url = html.escape(
f"https://github.com/settings/apps/{app_slug}/installations",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 5, definition_name: setup_callback
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -633,7 +633,7 @@
app_slug = urllib.parse.quote(str(credentials.get("slug", "stampbot")), safe="")
installation_url = html.escape(
f"https://github.com/settings/apps/{app_slug}/installations",
- quote=True,
+ quote=False,
)
html_content = f"""........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 6, definition_name: setup_status
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -726,6 +726,6 @@
_require_setup_access()
return {
"configured": is_configured(),
- "setup_enabled": True,
+ "setup_enabled": False,
}
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
....................F
=================================== FAILURES ===================================
________ TestSetupEndpointsUnconfigured.test_setup_status_omits_app_id _________
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7f1ddebd7ce0>
def test_setup_status_omits_app_id(self):
"""Test active setup status exposes no App identifier."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
from fastapi.testclient import TestClient
from stampbot.main import app
response = TestClient(app, raise_server_exceptions=False).get("/setup/status")
assert response.status_code == 200
> assert response.json() == {"configured": False, "setup_enabled": True}
E AssertionError: assert {'configured'...abled': False} == {'configured'...nabled': True}
E
E Omitting 1 identical items, use -vv to show
E Differing items:
E {'setup_enabled': False} != {'setup_enabled': True}
E
E Full diff:
E {
E 'configured': False,
E - 'setup_enabled': True,
E ? ^^^
E + 'setup_enabled': False,
E ? ^^^^
E }
tests/test_setup_endpoints.py:317: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup/status \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:16:53.982669Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup/status "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_setup_status_omits_app_id - AssertionError: assert {'configured'...abled': False} == {'configured'...nabled': True}
Omitting 1 identical items, use -vv to show
Differing items:
{'setup_enabled': False} != {'setup_enabled': True}
Full diff:
{
'configured': False,
- 'setup_enabled': True,
? ^^^
+ 'setup_enabled': False,
? ^^^^
}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 308 passed, 3 deselected, 1 warning in 3.92s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 0, definition_name: _setup_requested
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -72,7 +72,7 @@
def _setup_requested() -> bool:
"""Return whether the operator explicitly enabled setup."""
- return bool(settings.get("setup_enabled", False))
+ return bool(settings.get("setup_enabled", True))
def _setup_allowed_when_configured() -> bool:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.16s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 1, definition_name: _setup_allowed_when_configured
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -77,7 +77,7 @@
def _setup_allowed_when_configured() -> bool:
"""Return whether setup was explicitly reopened after configuration."""
- return bool(settings.get("setup_allow_configured", False))
+ return bool(settings.get("setup_allow_configured", True))
def _setup_is_available(configured: bool | None = None) -> bool:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 2, definition_name: _setup_is_available
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -83,7 +83,7 @@
def _setup_is_available(configured: bool | None = None) -> bool:
"""Return whether setup routes should be available for the current state."""
if not _setup_requested():
- return False
+ return True
app_configured = is_configured() if configured is None else configured
return not app_configured or _setup_allowed_when_configured()........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................................F
=================================== FAILURES ===================================
_______________ test_ready_endpoint_unconfigured_setup_disabled ________________
test_client = <starlette.testclient.TestClient object at 0x7f654cb9ff00>
def test_ready_endpoint_unconfigured_setup_disabled(test_client: TestClient):
"""Test readiness is 503 only when unconfigured and setup is disabled."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.get.side_effect = lambda key, default=None: {
"setup_enabled": False,
"setup_allow_configured": False,
}.get(key, default)
response = test_client.get("/ready")
> assert response.status_code == 503
E assert 200 == 503
E + where 200 = <Response [200 OK]>.status_code
tests/test_main.py:140: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:29:57.729823Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_unconfigured_setup_disabled - assert 200 == 503
+ where 200 = <Response [200 OK]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 205 passed, 3 deselected, 1 warning in 3.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 3, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -147,7 +147,7 @@
configured = is_configured()
if not configured:
logger.warning("GitHub App credentials not configured.")
- if _setup_is_available(configured=False):
+ if _setup_is_available(configured=True):
logger.info("Setup is enabled; visit /setup to create your GitHub App")
else:
logger.warning("Setup is disabled; enable it explicitly to provision the App")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 4, definition_name: root
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -347,7 +347,7 @@
Redirect to /setup if unconfigured, otherwise JSON status response.
"""
configured = is_configured()
- if not configured and _setup_is_available(configured=False):
+ if not configured and _setup_is_available(configured=True):
return RedirectResponse(url="/setup", status_code=307)
return JSONResponse(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...............F
=================================== FAILURES ===================================
_ TestSetupEndpointsUnconfigured.test_root_redirects_to_setup_when_unconfigured _
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7f0db3f02490>
def test_root_redirects_to_setup_when_unconfigured(self):
"""Test root redirects to /setup when not configured."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/", follow_redirects=False)
> assert response.status_code == 307
E assert 200 == 307
E + where 200 = <Response [200 OK]>.status_code
tests/test_setup_endpoints.py:187: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T23:04:08.308243Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_root_redirects_to_setup_when_unconfigured - assert 200 == 307
+ where 200 = <Response [200 OK]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 303 passed, 3 deselected, 1 warning in 3.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 0, definition_name: _require_setup_access
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -97,7 +97,7 @@
"""
if not _setup_requested():
raise HTTPException(status_code=403, detail="Setup is disabled")
- if is_configured() and not _setup_allowed_when_configured():
+ if is_configured() or not _setup_allowed_when_configured():
raise HTTPException(
status_code=403,
detail="Setup is closed because Stampbot is already configured",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............F
=================================== FAILURES ===================================
_____ TestSetupEndpointsConfigured.test_setup_can_be_deliberately_reopened _____
self = <tests.test_setup_endpoints.TestSetupEndpointsConfigured object at 0x7f5f4c17cd60>
def test_setup_can_be_deliberately_reopened(self):
"""Test configured setup requires the second explicit opt-in."""
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(
mock_settings,
enabled=True,
base_url="https://configured.example.test",
allow_configured=True,
)
from fastapi.testclient import TestClient
from stampbot.main import app
response = TestClient(app, raise_server_exceptions=False).get("/setup")
> assert response.status_code == 200
E assert 403 == 200
E + where 403 = <Response [403 Forbidden]>.status_code
tests/test_setup_endpoints.py:120: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 403 Forbidden\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:58:27.872704Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 403 Forbidden"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsConfigured::test_setup_can_be_deliberately_reopened - assert 403 == 200
+ where 403 = <Response [403 Forbidden]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 300 passed, 3 deselected, 1 warning in 3.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 1, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -221,7 +221,7 @@
continue
if match is Match.FULL:
return route_path
- if match is Match.PARTIAL and partial_match is None:
+ if match is Match.PARTIAL or partial_match is None:
# A path match with the wrong method will become a 405 response.
partial_match = route_path
........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7fc98eba6660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140503661486032'>, 'http_request_duration_seconds': ...='140503661484688'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140503661484016'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
response = test_client.post(path, content=b"request body")
assert response.status_code == 404
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == [
UNMATCHED_ENDPOINT,
UNMATCHED_ENDPOINT,
]
E AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
E
E At index 0 diff: '/openapi.json' != 'unmatched'
E
E Full diff:
E [
E - 'unmatched',
E - 'unmatched',
E + '/openapi.json',
E + '/openapi.json',
E ]
tests/test_http_metrics.py:101: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:08:13.727928Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T22:08:13.729961Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - AssertionError: assert ['/openapi.js...openapi.json'] == ['unmatched', 'unmatched']
At index 0 diff: '/openapi.json' != 'unmatched'
Full diff:
[
- 'unmatched',
- 'unmatched',
+ '/openapi.json',
+ '/openapi.json',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 2, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -327,7 +327,7 @@
# entry is the original client (proxies append to the right).
client_ip = raw.split(",")[0].strip()
- if not client_ip and scope.get("client"):
+ if not client_ip or scope.get("client"):
client_ip = scope["client"][0]
if client_ip:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........F
=================================== FAILURES ===================================
______________ test_logging_middleware_respects_configured_header ______________
def test_logging_middleware_respects_configured_header():
"""Test that logging_middleware uses the configured client_ip_header setting."""
from unittest.mock import patch
import structlog.contextvars
from stampbot.main import app
with patch("stampbot.main.settings") as mock_settings:
mock_settings.get = lambda key, default=None: (
"X-Real-IP" if key == "client_ip_header" else default
)
client = TestClient(app)
captured: dict[str, str | None] = {}
original_bind = structlog.contextvars.bind_contextvars
def capturing_bind(**kw: object) -> None:
captured.update({k: str(v) for k, v in kw.items()})
original_bind(**kw)
with patch("stampbot.main.structlog.contextvars.bind_contextvars", capturing_bind):
client.get("/health", headers={"X-Real-IP": "198.51.100.7"})
> assert captured.get("client_ip") == "198.51.100.7"
E AssertionError: assert 'testclient' == '198.51.100.7'
E
E - 198.51.100.7
E + testclient
tests/test_main.py:481: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/health \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:19:45.152122Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/health "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_logging_middleware_respects_configured_header - AssertionError: assert 'testclient' == '198.51.100.7'
- 198.51.100.7
+ testclient
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 224 passed, 3 deselected, 1 warning in 3.57s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceAndWithOr, occurrence: 3, definition_name: root
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -347,7 +347,7 @@
Redirect to /setup if unconfigured, otherwise JSON status response.
"""
configured = is_configured()
- if not configured and _setup_is_available(configured=False):
+ if not configured or _setup_is_available(configured=False):
return RedirectResponse(url="/setup", status_code=307)
return JSONResponse(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 4, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -459,7 +459,7 @@
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
- if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
+ if content_length or int(content_length) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f21b5393680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 413 == 401
E + where 413 = <Response [413 Request Entity Too Large]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 413 Request Entity Too Large\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:42:52.304331Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 413 Request Entity Too Large"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 413 == 401
+ where 413 = <Response [413 Request Entity Too Large]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.54s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 0, definition_name: _setup_is_available
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -86,7 +86,7 @@
return False
app_configured = is_configured() if configured is None else configured
- return not app_configured or _setup_allowed_when_configured()
+ return not app_configured and _setup_allowed_when_configured()
def _require_setup_access() -> None:........................................................................ [ 15%]
........................................................................ [ 31%]
............................................................F
=================================== FAILURES ===================================
________________ test_ready_endpoint_unconfigured_setup_enabled ________________
test_client = <starlette.testclient.TestClient object at 0x7fdaa71af680>
def test_ready_endpoint_unconfigured_setup_enabled(test_client: TestClient):
"""Test readiness is 200 in setup mode so /setup stays reachable.
An unconfigured pod with setup mode on must stay in the Service endpoints,
otherwise the operator can never reach /setup to configure it.
"""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.get.side_effect = lambda key, default=None: {
"setup_enabled": True,
"setup_allow_configured": False,
}.get(key, default)
response = test_client.get("/ready")
> assert response.status_code == 200
E assert 503 == 200
E + where 503 = <Response [503 Service Unavailable]>.status_code
tests/test_main.py:123: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 503 Service Unavailable\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:15:03.748155Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 503 Service Unavailable"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_unconfigured_setup_enabled - assert 503 == 200
+ where 503 = <Response [503 Service Unavailable]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 204 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 1, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -225,7 +225,7 @@
# A path match with the wrong method will become a 405 response.
partial_match = route_path
- return partial_match or UNMATCHED_ENDPOINT
+ return partial_match and UNMATCHED_ENDPOINT
@app.middleware("http")........................................................................ [ 15%]
........................................................................ [ 31%]
...............................F
=================================== FAILURES ===================================
_______________ test_route_without_a_path_uses_bounded_fallback ________________
def test_route_without_a_path_uses_bounded_fallback() -> None:
"""Custom framework routes without path templates cannot leak raw paths."""
route = MagicMock()
route.matches.return_value = (object(), {"route": SimpleNamespace(path=None)})
request = Request(
{
"type": "http",
"app": SimpleNamespace(routes=[route]),
"path": "/raw-user-controlled-path",
}
)
> assert _metric_endpoint_label(request) == UNMATCHED_ENDPOINT
E AssertionError: assert None == 'unmatched'
E + where None = _metric_endpoint_label(<starlette.requests.Request object at 0x7efc84e0d090>)
tests/test_http_metrics.py:84: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_route_without_a_path_uses_bounded_fallback - AssertionError: assert None == 'unmatched'
+ where None = _metric_endpoint_label(<starlette.requests.Request object at 0x7efc84e0d090>)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 175 passed, 3 deselected, 1 warning in 2.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 2, definition_name: ready
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -394,7 +394,7 @@
configured = is_configured()
setup_enabled = _setup_is_available(configured=configured)
checks = {"configured": configured, "setup_enabled": setup_enabled}
- is_ready = configured or setup_enabled
+ is_ready = configured and setup_enabled
return JSONResponse(
status_code=200 if is_ready else 503,
content={"status": "ready" if is_ready else "not ready", "checks": checks},........................................................................ [ 15%]
........................................................................ [ 31%]
...........................................................F
=================================== FAILURES ===================================
________________________ test_ready_endpoint_configured ________________________
test_client = <starlette.testclient.TestClient object at 0x7f0ec28a7350>
def test_ready_endpoint_configured(test_client: TestClient):
"""Test readiness endpoint returns 200 when the app is configured."""
with patch("stampbot.main.is_configured", return_value=True):
response = test_client.get("/ready")
> assert response.status_code == 200
E assert 503 == 200
E + where 503 = <Response [503 Service Unavailable]>.status_code
tests/test_main.py:102: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 503 Service Unavailable\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:20:08.605893Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 503 Service Unavailable"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_configured - assert 503 == 200
+ where 503 = <Response [503 Service Unavailable]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 203 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 3, definition_name: _delivery_id_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -417,7 +417,7 @@
"""
if not raw:
return None
- if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
+ if len(raw) > MAX_DELIVERY_ID_LENGTH and not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.............F
=================================== FAILURES ===================================
_ test_webhook_drops_malformed_delivery_id[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] _
test_client = <starlette.testclient.TestClient object at 0x7fb47d859e10>
header = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
@pytest.mark.parametrize("header", ["", "x" * 65, "not a guid!", "../../etc"])
def test_webhook_drops_malformed_delivery_id(test_client: TestClient, header: str):
"""Test a delivery header that is not a GUID is neither logged nor forwarded."""
body = json.dumps({"zen": "test"}).encode()
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": header,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] is None
E AssertionError: assert 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is None
tests/test_main.py:606: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:54:18.005285Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_drops_malformed_delivery_id[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] - AssertionError: assert 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 229 passed, 3 deselected, 1 warning in 3.57s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 4, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -502,7 +502,7 @@
)
duration = time.time() - start_time
- webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
+ webhook_processing_duration_seconds.labels(event_type=x_github_event and "unknown").observe(
duration
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceContinueWithBreak, occurrence: 0, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -218,7 +218,7 @@
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)
if not isinstance(route_path, str):
- continue
+ break
if match is Match.FULL:
return route_path
if match is Match.PARTIAL and partial_match is None:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 0, definition_name: _trusted_setup_base_url
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -113,7 +113,7 @@
configured_base_url = settings.get("base_url", "")
try:
return validate_base_url(configured_base_url)
- except ValueError as error:
+ except CosmicRayTestingException as error:
logger.warning("Setup base URL is missing or invalid: %s", error)
raise HTTPException(
status_code=503,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................F
=================================== FAILURES ===================================
____ TestSetupEndpointsUnconfigured.test_setup_requires_configured_base_url ____
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7fb23a695350>
def test_setup_requires_configured_base_url(self):
"""Test request headers cannot supply a missing trusted setup URL."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.get(
"/setup",
headers={
"Host": "internal-service:8000",
"X-Forwarded-Proto": "https",
"X-Forwarded-Host": "untrusted.example.test",
},
)
> assert response.status_code == 503
E assert 500 == 503
E + where 500 = <Response [500 Internal Server Error]>.status_code
tests/test_setup_endpoints.py:242: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 500 Internal Server Error\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:26:22.574334Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 500 Internal Server Error"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_setup_requires_configured_base_url - assert 500 == 503
+ where 500 = <Response [500 Internal Server Error]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 305 passed, 3 deselected, 1 warning in 3.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 1, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -483,7 +483,7 @@
# Parse JSON payload
try:
payload = await request.json()
- except Exception as e:
+ except CosmicRayTestingException as e:
errors_total.labels(error_type="payload_invalid").inc()
logger.error("Failed to parse webhook payload: %s", e)
raise HTTPException(status_code=400, detail="Invalid JSON payload") from None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.F
=================================== FAILURES ===================================
__________________________ test_webhook_invalid_json ___________________________
request = <starlette.requests.Request object at 0x7f738c511f40>
x_github_event = 'ping'
x_hub_signature_256 = 'sha256=dc481541cb8394ab16852fc7c7cbce720b045e7456801ae3998a45f260ad2915'
x_github_delivery = None
@app.post("/webhook")
async def webhook(
request: Request,
x_github_event: str = Header(None, alias="X-GitHub-Event"),
x_hub_signature_256: str = Header(None, alias="X-Hub-Signature-256"),
x_github_delivery: str = Header(None, alias="X-GitHub-Delivery"),
) -> dict[str, Any]:
"""GitHub webhook endpoint.
Args:
request: FastAPI request.
x_github_event: GitHub event type.
x_hub_signature_256: Webhook signature.
x_github_delivery: GitHub delivery GUID, bound to logs and traces after
the signature is verified so a delivery in GitHub's log can be
matched to Stampbot's records.
Returns:
Response dictionary with status and message.
Raises:
HTTPException: 400 if event header or JSON invalid, 401 if signature invalid,
413 if body too large, 503 if not configured, 500 on internal error.
"""
# Check if app is configured
if not is_configured():
raise HTTPException(
status_code=503,
detail="Stampbot not configured. Visit /setup to complete setup.",
)
if not x_github_event:
errors_total.labels(error_type="missing_event").inc()
raise HTTPException(status_code=400, detail="Missing X-GitHub-Event header")
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
# Get raw body for signature verification
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
if len(body) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
# Verify signature
if not webhook_handler.verify_signature(body, x_hub_signature_256):
webhook_signature_validations_total.labels(result="invalid").inc()
errors_total.labels(error_type="signature_invalid").inc()
logger.warning("Invalid webhook signature")
raise HTTPException(status_code=401, detail="Invalid signature")
webhook_signature_validations_total.labels(result="valid").inc()
# Parse JSON payload
try:
> payload = await request.json()
^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:485:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/requests.py:265: in json
self._json = json.loads(body)
^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/json/__init__.py:352: in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/json/decoder.py:345: in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <json.decoder.JSONDecoder object at 0x7f739436b380>, s = 'not valid json'
idx = 0
def raw_decode(self, s, idx=0):
"""Decode a JSON document from ``s`` (a ``str`` beginning with
a JSON document) and return a 2-tuple of the Python
representation and the index in ``s`` where the document ended.
This can be used to decode a JSON document from a string that may
have extraneous data at the end.
"""
try:
obj, end = self.scan_once(s, idx)
except StopIteration as err:
> raise JSONDecodeError("Expecting value", s, err.value) from None
E json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/json/decoder.py:363: JSONDecodeError
The above exception was the direct cause of the following exception:
test_client = <starlette.testclient.TestClient object at 0x7f738c5e4160>
def test_webhook_invalid_json(test_client: TestClient):
"""Test webhook rejects invalid JSON payload."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
# Create a valid signature for invalid JSON
body = b"not valid json"
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
> response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
tests/test_main.py:321:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.requests.Request object at 0x7f738c511f40>
x_github_event = 'ping'
x_hub_signature_256 = 'sha256=dc481541cb8394ab16852fc7c7cbce720b045e7456801ae3998a45f260ad2915'
x_github_delivery = None
@app.post("/webhook")
async def webhook(
request: Request,
x_github_event: str = Header(None, alias="X-GitHub-Event"),
x_hub_signature_256: str = Header(None, alias="X-Hub-Signature-256"),
x_github_delivery: str = Header(None, alias="X-GitHub-Delivery"),
) -> dict[str, Any]:
"""GitHub webhook endpoint.
Args:
request: FastAPI request.
x_github_event: GitHub event type.
x_hub_signature_256: Webhook signature.
x_github_delivery: GitHub delivery GUID, bound to logs and traces after
the signature is verified so a delivery in GitHub's log can be
matched to Stampbot's records.
Returns:
Response dictionary with status and message.
Raises:
HTTPException: 400 if event header or JSON invalid, 401 if signature invalid,
413 if body too large, 503 if not configured, 500 on internal error.
"""
# Check if app is configured
if not is_configured():
raise HTTPException(
status_code=503,
detail="Stampbot not configured. Visit /setup to complete setup.",
)
if not x_github_event:
errors_total.labels(error_type="missing_event").inc()
raise HTTPException(status_code=400, detail="Missing X-GitHub-Event header")
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
# Get raw body for signature verification
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
if len(body) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
# Verify signature
if not webhook_handler.verify_signature(body, x_hub_signature_256):
webhook_signature_validations_total.labels(result="invalid").inc()
errors_total.labels(error_type="signature_invalid").inc()
logger.warning("Invalid webhook signature")
raise HTTPException(status_code=401, detail="Invalid signature")
webhook_signature_validations_total.labels(result="valid").inc()
# Parse JSON payload
try:
payload = await request.json()
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/main.py:486: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_invalid_json - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 217 passed, 3 deselected, 1 warning in 4.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 2, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -507,7 +507,7 @@
)
return result
- except Exception as e:
+ except CosmicRayTestingException as e:
errors_total.labels(error_type="webhook_handler_error").inc()
sanitized = _sanitize_error(e)
logger.error("Error handling webhook event: %s", sanitized, extra={"error": sanitized})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..........F
=================================== FAILURES ===================================
________________________ test_webhook_handler_exception ________________________
request = <starlette.requests.Request object at 0x7fb6bf747330>
x_github_event = 'ping'
x_hub_signature_256 = 'sha256=595bde46b32458346f405d2b0b88ade07c42ddd464fa4cb8df965915cfa558d2'
x_github_delivery = None
@app.post("/webhook")
async def webhook(
request: Request,
x_github_event: str = Header(None, alias="X-GitHub-Event"),
x_hub_signature_256: str = Header(None, alias="X-Hub-Signature-256"),
x_github_delivery: str = Header(None, alias="X-GitHub-Delivery"),
) -> dict[str, Any]:
"""GitHub webhook endpoint.
Args:
request: FastAPI request.
x_github_event: GitHub event type.
x_hub_signature_256: Webhook signature.
x_github_delivery: GitHub delivery GUID, bound to logs and traces after
the signature is verified so a delivery in GitHub's log can be
matched to Stampbot's records.
Returns:
Response dictionary with status and message.
Raises:
HTTPException: 400 if event header or JSON invalid, 401 if signature invalid,
413 if body too large, 503 if not configured, 500 on internal error.
"""
# Check if app is configured
if not is_configured():
raise HTTPException(
status_code=503,
detail="Stampbot not configured. Visit /setup to complete setup.",
)
if not x_github_event:
errors_total.labels(error_type="missing_event").inc()
raise HTTPException(status_code=400, detail="Missing X-GitHub-Event header")
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
# Get raw body for signature verification
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
if len(body) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
# Verify signature
if not webhook_handler.verify_signature(body, x_hub_signature_256):
webhook_signature_validations_total.labels(result="invalid").inc()
errors_total.labels(error_type="signature_invalid").inc()
logger.warning("Invalid webhook signature")
raise HTTPException(status_code=401, detail="Invalid signature")
webhook_signature_validations_total.labels(result="valid").inc()
# Parse JSON payload
try:
payload = await request.json()
except Exception as e:
errors_total.labels(error_type="payload_invalid").inc()
logger.error("Failed to parse webhook payload: %s", e)
raise HTTPException(status_code=400, detail="Invalid JSON payload") from None
# The header is only trusted once the HMAC check has passed. Bound length
# keeps a malformed value out of log storage.
delivery_id = _delivery_id_label(x_github_delivery)
if delivery_id:
structlog.contextvars.bind_contextvars(delivery_id=delivery_id)
# Handle event with timing
try:
start_time = time.time()
> result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
stampbot/main.py:500:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <AsyncMock name='handle_event' id='140422872425072'>
args = ('ping', {'zen': 'test'}), kwargs = {'delivery_id': None}
async def _execute_mock_call(self, /, *args, **kwargs):
# This is nearly just like super(), except for special handling
# of coroutines
_call = _Call((args, kwargs), two=True)
self.await_count += 1
self.await_args = _call
self.await_args_list.append(_call)
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Unexpected error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:2333: Exception
The above exception was the direct cause of the following exception:
test_client = <starlette.testclient.TestClient object at 0x7fb6bf7c2580>
def test_webhook_handler_exception(test_client: TestClient):
"""Test webhook returns 500 when handler raises exception."""
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "test"}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
with patch("stampbot.main.webhook_handler.handle_event") as mock_handle:
mock_handle.side_effect = Exception("Unexpected error")
> response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
tests/test_main.py:535:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <starlette.requests.Request object at 0x7fb6bf747330>
x_github_event = 'ping'
x_hub_signature_256 = 'sha256=595bde46b32458346f405d2b0b88ade07c42ddd464fa4cb8df965915cfa558d2'
x_github_delivery = None
@app.post("/webhook")
async def webhook(
request: Request,
x_github_event: str = Header(None, alias="X-GitHub-Event"),
x_hub_signature_256: str = Header(None, alias="X-Hub-Signature-256"),
x_github_delivery: str = Header(None, alias="X-GitHub-Delivery"),
) -> dict[str, Any]:
"""GitHub webhook endpoint.
Args:
request: FastAPI request.
x_github_event: GitHub event type.
x_hub_signature_256: Webhook signature.
x_github_delivery: GitHub delivery GUID, bound to logs and traces after
the signature is verified so a delivery in GitHub's log can be
matched to Stampbot's records.
Returns:
Response dictionary with status and message.
Raises:
HTTPException: 400 if event header or JSON invalid, 401 if signature invalid,
413 if body too large, 503 if not configured, 500 on internal error.
"""
# Check if app is configured
if not is_configured():
raise HTTPException(
status_code=503,
detail="Stampbot not configured. Visit /setup to complete setup.",
)
if not x_github_event:
errors_total.labels(error_type="missing_event").inc()
raise HTTPException(status_code=400, detail="Missing X-GitHub-Event header")
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")
if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
# Get raw body for signature verification
body = await request.body()
# Double-check actual body size (in case content-length was missing/incorrect)
if len(body) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
raise HTTPException(status_code=413, detail="Request body too large")
# Verify signature
if not webhook_handler.verify_signature(body, x_hub_signature_256):
webhook_signature_validations_total.labels(result="invalid").inc()
errors_total.labels(error_type="signature_invalid").inc()
logger.warning("Invalid webhook signature")
raise HTTPException(status_code=401, detail="Invalid signature")
webhook_signature_validations_total.labels(result="valid").inc()
# Parse JSON payload
try:
payload = await request.json()
except Exception as e:
errors_total.labels(error_type="payload_invalid").inc()
logger.error("Failed to parse webhook payload: %s", e)
raise HTTPException(status_code=400, detail="Invalid JSON payload") from None
# The header is only trusted once the HMAC check has passed. Bound length
# keeps a malformed value out of log storage.
delivery_id = _delivery_id_label(x_github_delivery)
if delivery_id:
structlog.contextvars.bind_contextvars(delivery_id=delivery_id)
# Handle event with timing
try:
start_time = time.time()
result = await webhook_handler.handle_event(
x_github_event, payload, delivery_id=delivery_id
)
duration = time.time() - start_time
webhook_processing_duration_seconds.labels(event_type=x_github_event or "unknown").observe(
duration
)
return result
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/main.py:510: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_handler_exception - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 226 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 3, definition_name: setup_callback
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -615,7 +615,7 @@
try:
credentials = await exchange_code_for_credentials(code)
- except Exception as e:
+ except CosmicRayTestingException as e:
logger.error("Failed to exchange code for credentials: %s", _sanitize_error(e))
raise HTTPException(status_code=500, detail="Failed to complete setup") from None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................F
=================================== FAILURES ===================================
____________ TestSetupCallback.test_callback_handles_exchange_error ____________
code = 'bad-code'
@app.get("/setup/callback")
async def setup_callback(code: str) -> Response:
"""Handle callback from GitHub after app creation.
Args:
code: Temporary code from GitHub to exchange for credentials.
Returns:
HTML page with credentials and setup instructions.
Raises:
HTTPException: If setup disabled (403) or code exchange fails (500).
"""
_require_setup_access()
try:
> credentials = await exchange_code_for_credentials(code)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:617:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <AsyncMock name='exchange_code_for_credentials' id='139728733589184'>
args = ('bad-code',), kwargs = {}
async def _execute_mock_call(self, /, *args, **kwargs):
# This is nearly just like super(), except for special handling
# of coroutines
_call = _Call((args, kwargs), two=True)
self.await_count += 1
self.await_args = _call
self.await_args_list.append(_call)
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:2333: Exception
The above exception was the direct cause of the following exception:
self = <tests.test_setup_endpoints.TestSetupCallback object at 0x7f1521e455b0>
test_client = <starlette.testclient.TestClient object at 0x7f15219b1bf0>
def test_callback_handles_exchange_error(self, test_client):
"""Test callback handles exchange error gracefully."""
with (
patch("stampbot.main._require_setup_access"),
patch(
"stampbot.main.exchange_code_for_credentials",
new_callable=AsyncMock,
side_effect=Exception("API error"),
),
):
> response = test_client.get("/setup/callback?code=bad-code")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_setup_endpoints.py:404:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:482: in get
return super().get(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1053: in get
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
code = 'bad-code'
@app.get("/setup/callback")
async def setup_callback(code: str) -> Response:
"""Handle callback from GitHub after app creation.
Args:
code: Temporary code from GitHub to exchange for credentials.
Returns:
HTML page with credentials and setup instructions.
Raises:
HTTPException: If setup disabled (403) or code exchange fails (500).
"""
_require_setup_access()
try:
credentials = await exchange_code_for_credentials(code)
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/main.py:618: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupCallback::test_callback_handles_exchange_error - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 312 passed, 3 deselected, 1 warning in 4.40s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1025 * 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 1, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1023 * 1024 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 2, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 * 1025 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 3, definition_name: None
--- mutation diff --- --- a/stampbot/main.py +++ b/stampbot/main.py @@ -55,7 +55,7 @@ configure_telemetry() # Security limits -MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller +MAX_WEBHOOK_BODY_SIZE = 1024 * 1023 # 1MB - GitHub webhooks are typically much smaller # X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958. MAX_DELIVERY_ID_LENGTH = 64 DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 4, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -57,7 +57,7 @@
# Security limits
MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller
# X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958.
-MAX_DELIVERY_ID_LENGTH = 64
+MAX_DELIVERY_ID_LENGTH = 65
DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
UNMATCHED_ENDPOINT = "unmatched"
SETUP_HTML_HEADERS = {........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.............F
=================================== FAILURES ===================================
_ test_webhook_drops_malformed_delivery_id[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] _
test_client = <starlette.testclient.TestClient object at 0x7f62d5119e10>
header = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
@pytest.mark.parametrize("header", ["", "x" * 65, "not a guid!", "../../etc"])
def test_webhook_drops_malformed_delivery_id(test_client: TestClient, header: str):
"""Test a delivery header that is not a GUID is neither logged nor forwarded."""
body = json.dumps({"zen": "test"}).encode()
seen_context: dict[str, object] = {}
async def fake_handle(event_type, payload, delivery_id=None):
seen_context.update(structlog.contextvars.get_contextvars())
return {"status": "ok", "message": event_type, "delivery": delivery_id}
with patch("stampbot.main.webhook_handler.handle_event", side_effect=fake_handle):
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-GitHub-Delivery": header,
"X-Hub-Signature-256": _signed_ping(body),
},
)
assert response.status_code == 200
> assert response.json()["delivery"] is None
E AssertionError: assert 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is None
tests/test_main.py:606: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:22:42.105601Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_drops_malformed_delivery_id[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] - AssertionError: assert 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 229 passed, 3 deselected, 1 warning in 3.60s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 5, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -57,7 +57,7 @@
# Security limits
MAX_WEBHOOK_BODY_SIZE = 1024 * 1024 # 1MB - GitHub webhooks are typically much smaller
# X-GitHub-Delivery is a GUID such as 72d3162e-cc78-11e3-81ab-4c9367dc0958.
-MAX_DELIVERY_ID_LENGTH = 64
+MAX_DELIVERY_ID_LENGTH = 63
DELIVERY_ID_PATTERN = re.compile(r"[A-Za-z0-9-]+")
UNMATCHED_ENDPOINT = "unmatched"
SETUP_HTML_HEADERS = {........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 6, definition_name: _require_setup_access
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -96,7 +96,7 @@
HTTPException: If setup is disabled or automatically closed.
"""
if not _setup_requested():
- raise HTTPException(status_code=403, detail="Setup is disabled")
+ raise HTTPException(status_code= 404, detail="Setup is disabled")
if is_configured() and not _setup_allowed_when_configured():
raise HTTPException(
status_code=403,........................................................................ [ 15%]
........................................................................ [ 31%]
...................................F
=================================== FAILURES ===================================
________________ test_setup_routes_keep_distinct_static_labels _________________
test_client = <starlette.testclient.TestClient object at 0x7f1d24766780>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='139763138271216'>, 'http_request_duration_seconds': ...='139763138271888'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='139763138272224'>, ...}
def test_setup_routes_keep_distinct_static_labels(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Setup and callback traffic remains distinguishable by route."""
setup_response = test_client.get("/setup")
callback_response = test_client.get("/setup/callback")
> assert setup_response.status_code == 403
E assert 404 == 403
E + where 404 = <Response [404 Not Found]>.status_code
tests/test_http_metrics.py:143: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:25:38.524160Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/setup "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T21:25:38.526647Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/setup/callback "HTTP/1.1 422 Unprocessable Entity"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup/callback "HTTP/1.1 422 Unprocessable Entity"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_setup_routes_keep_distinct_static_labels - assert 404 == 403
+ where 404 = <Response [404 Not Found]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 179 passed, 3 deselected, 1 warning in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 7, definition_name: _require_setup_access
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -96,7 +96,7 @@
HTTPException: If setup is disabled or automatically closed.
"""
if not _setup_requested():
- raise HTTPException(status_code=403, detail="Setup is disabled")
+ raise HTTPException(status_code= 402, detail="Setup is disabled")
if is_configured() and not _setup_allowed_when_configured():
raise HTTPException(
status_code=403,........................................................................ [ 15%]
........................................................................ [ 31%]
...................................F
=================================== FAILURES ===================================
________________ test_setup_routes_keep_distinct_static_labels _________________
test_client = <starlette.testclient.TestClient object at 0x7fa1783fa780>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140331411544048'>, 'http_request_duration_seconds': ...='140331411544720'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140331411545056'>, ...}
def test_setup_routes_keep_distinct_static_labels(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Setup and callback traffic remains distinguishable by route."""
setup_response = test_client.get("/setup")
callback_response = test_client.get("/setup/callback")
> assert setup_response.status_code == 403
E assert 402 == 403
E + where 402 = <Response [402 Payment Required]>.status_code
tests/test_http_metrics.py:143: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:35:01.452538Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/setup "HTTP/1.1 402 Payment Required"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T20:35:01.455156Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/setup/callback "HTTP/1.1 422 Unprocessable Entity"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 402 Payment Required"
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup/callback "HTTP/1.1 422 Unprocessable Entity"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_setup_routes_keep_distinct_static_labels - assert 402 == 403
+ where 402 = <Response [402 Payment Required]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 179 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 8, definition_name: _require_setup_access
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -99,7 +99,7 @@
raise HTTPException(status_code=403, detail="Setup is disabled")
if is_configured() and not _setup_allowed_when_configured():
raise HTTPException(
- status_code=403,
+ status_code= 404,
detail="Setup is closed because Stampbot is already configured",
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...........F
=================================== FAILURES ===================================
_____ TestSetupEndpointsConfigured.test_setup_auto_closes_when_configured ______
self = <tests.test_setup_endpoints.TestSetupEndpointsConfigured object at 0x7faeead8e350>
def test_setup_auto_closes_when_configured(self):
"""Test an initial-setup opt-in does not keep setup open after configuration."""
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(
mock_settings,
enabled=True,
base_url="https://configured.example.test",
)
from fastapi.testclient import TestClient
from stampbot.main import app
response = TestClient(app, raise_server_exceptions=False).get("/setup")
> assert response.status_code == 403
E assert 404 == 403
E + where 404 = <Response [404 Not Found]>.status_code
tests/test_setup_endpoints.py:98: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 404 Not Found\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:55:59.394813Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsConfigured::test_setup_auto_closes_when_configured - assert 404 == 403
+ where 404 = <Response [404 Not Found]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 299 passed, 3 deselected, 1 warning in 3.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 9, definition_name: _require_setup_access
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -99,7 +99,7 @@
raise HTTPException(status_code=403, detail="Setup is disabled")
if is_configured() and not _setup_allowed_when_configured():
raise HTTPException(
- status_code=403,
+ status_code= 402,
detail="Setup is closed because Stampbot is already configured",
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...........F
=================================== FAILURES ===================================
_____ TestSetupEndpointsConfigured.test_setup_auto_closes_when_configured ______
self = <tests.test_setup_endpoints.TestSetupEndpointsConfigured object at 0x7f2700f76350>
def test_setup_auto_closes_when_configured(self):
"""Test an initial-setup opt-in does not keep setup open after configuration."""
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(
mock_settings,
enabled=True,
base_url="https://configured.example.test",
)
from fastapi.testclient import TestClient
from stampbot.main import app
response = TestClient(app, raise_server_exceptions=False).get("/setup")
> assert response.status_code == 403
E assert 402 == 403
E + where 402 = <Response [402 Payment Required]>.status_code
tests/test_setup_endpoints.py:98: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 402 Payment Required\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:29:01.745197Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 402 Payment Required"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsConfigured::test_setup_auto_closes_when_configured - assert 402 == 403
+ where 402 = <Response [402 Payment Required]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 299 passed, 3 deselected, 1 warning in 3.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 10, definition_name: _trusted_setup_base_url
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -116,7 +116,7 @@
except ValueError as error:
logger.warning("Setup base URL is missing or invalid: %s", error)
raise HTTPException(
- status_code=503,
+ status_code= 504,
detail=("Setup requires STAMPBOT_BASE_URL to be set to Stampbot's trusted public URL"),
) from None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................F
=================================== FAILURES ===================================
____ TestSetupEndpointsUnconfigured.test_setup_requires_configured_base_url ____
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7fbb28aa9350>
def test_setup_requires_configured_base_url(self):
"""Test request headers cannot supply a missing trusted setup URL."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.get(
"/setup",
headers={
"Host": "internal-service:8000",
"X-Forwarded-Proto": "https",
"X-Forwarded-Host": "untrusted.example.test",
},
)
> assert response.status_code == 503
E assert 504 == 503
E + where 504 = <Response [504 Gateway Timeout]>.status_code
tests/test_setup_endpoints.py:242: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Setup base URL is missing or invalid: Invalid base_url: must be configured", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T21:04:15.472383Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 504 Gateway Timeout\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:04:15.473295Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:117 {'event': 'Setup base URL is missing or invalid: Invalid base_url: must be configured', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T21:04:15.472383Z'}
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 504 Gateway Timeout"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_setup_requires_configured_base_url - assert 504 == 503
+ where 504 = <Response [504 Gateway Timeout]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 305 passed, 3 deselected, 1 warning in 3.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 11, definition_name: _trusted_setup_base_url
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -116,7 +116,7 @@
except ValueError as error:
logger.warning("Setup base URL is missing or invalid: %s", error)
raise HTTPException(
- status_code=503,
+ status_code= 502,
detail=("Setup requires STAMPBOT_BASE_URL to be set to Stampbot's trusted public URL"),
) from None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................F
=================================== FAILURES ===================================
____ TestSetupEndpointsUnconfigured.test_setup_requires_configured_base_url ____
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7fa0e99c9350>
def test_setup_requires_configured_base_url(self):
"""Test request headers cannot supply a missing trusted setup URL."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.get(
"/setup",
headers={
"Host": "internal-service:8000",
"X-Forwarded-Proto": "https",
"X-Forwarded-Host": "untrusted.example.test",
},
)
> assert response.status_code == 503
E assert 502 == 503
E + where 502 = <Response [502 Bad Gateway]>.status_code
tests/test_setup_endpoints.py:242: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Setup base URL is missing or invalid: Invalid base_url: must be configured", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T23:04:37.629967Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: GET http://testserver/setup \"HTTP/1.1 502 Bad Gateway\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T23:04:37.630934Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:117 {'event': 'Setup base URL is missing or invalid: Invalid base_url: must be configured', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T23:04:37.629967Z'}
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 502 Bad Gateway"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_setup_requires_configured_base_url - assert 502 == 503
+ where 502 = <Response [502 Bad Gateway]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 305 passed, 3 deselected, 1 warning in 4.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 12, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 2 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 13, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 0 <= metrics_port <= 65535:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
...................................................................F
=================================== FAILURES ===================================
_ test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-0-metrics_port must be between 1 and 65535] _
metrics_host = '127.0.0.1', metrics_port = 0
message = 'metrics_port must be between 1 and 65535'
@pytest.mark.parametrize(
("metrics_host", "metrics_port", "message"),
[
("", 9090, "metrics_host must not be empty"),
("127.0.0.1", 0, "metrics_port must be between 1 and 65535"),
("127.0.0.1", 65536, "metrics_port must be between 1 and 65535"),
],
)
def test_lifespan_rejects_invalid_metrics_listener(
metrics_host: str,
metrics_port: int,
message: str,
):
"""Test that invalid dedicated-listener settings fail startup."""
from stampbot.main import app
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = metrics_host
mock_settings.metrics_port = metrics_port
> with pytest.raises(RuntimeError, match=message):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE RuntimeError
tests/test_main.py:246: Failed
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:36:34.103457Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:36:34.103655Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"extra": {"host": "127.0.0.1", "port": 0}, "event": "Prometheus metrics listener started", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:36:34.104048Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "Shutting down stampbot", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:36:34.604838Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:36:34.103457Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:36:34.103655Z'}
INFO stampbot.main:main.py:167 {'extra': {'host': '127.0.0.1', 'port': 0}, 'event': 'Prometheus metrics listener started', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:36:34.104048Z'}
INFO stampbot.main:main.py:178 {'event': 'Shutting down stampbot', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:36:34.604838Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-0-metrics_port must be between 1 and 65535] - Failed: DID NOT RAISE RuntimeError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 211 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 14, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 <= metrics_port <= 65536:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
....................................................................F
=================================== FAILURES ===================================
_ test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-65536-metrics_port must be between 1 and 65535] _
metrics_host = '127.0.0.1', metrics_port = 65536
message = 'metrics_port must be between 1 and 65535'
@pytest.mark.parametrize(
("metrics_host", "metrics_port", "message"),
[
("", 9090, "metrics_host must not be empty"),
("127.0.0.1", 0, "metrics_port must be between 1 and 65535"),
("127.0.0.1", 65536, "metrics_port must be between 1 and 65535"),
],
)
def test_lifespan_rejects_invalid_metrics_listener(
metrics_host: str,
metrics_port: int,
message: str,
):
"""Test that invalid dedicated-listener settings fail startup."""
from stampbot.main import app
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
mock_settings.metrics_enabled = True
mock_settings.metrics_host = metrics_host
mock_settings.metrics_port = metrics_port
with pytest.raises(RuntimeError, match=message):
> with TestClient(app):
^^^^^^^^^^^^^^^
tests/test_main.py:247:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:697: in __enter__
portal.call(self.wait_startup)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:732: in wait_startup
await receive()
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:723: in receive
self.task.result()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:713: in lifespan
await self.app(scope, self.stream_receive.receive, self.stream_send.send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:151: in __call__
await self.app(scope, receive, send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:103: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:49: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2726: in app
await self.lifespan(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:638: in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:166: in lifespan
metrics_runtime = start_metrics_server(metrics_host, metrics_port)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/metrics.py:194: in start_metrics_server
server, thread = start_http_server(port=port, addr=host, registry=registry)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/exposition.py:260: in start_wsgi_server
httpd = make_server(addr, port, app, TmpServer, handler_class=_SilentHandler)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/wsgiref/simple_server.py:150: in make_server
server = server_class((host, port), handler_class)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/socketserver.py:457: in __init__
self.server_bind()
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/wsgiref/simple_server.py:50: in server_bind
HTTPServer.server_bind(self)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/http/server.py:148: in server_bind
socketserver.TCPServer.server_bind(self)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <prometheus_client.exposition.start_wsgi_server.<locals>.TmpServer object at 0x7f7beab71a90>
def server_bind(self):
"""Called by constructor to bind the socket.
May be overridden.
"""
if self.allow_reuse_address and hasattr(socket, "SO_REUSEADDR"):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Since Linux 6.12.9, SO_REUSEPORT is not allowed
# on other address families than AF_INET/AF_INET6.
if (
self.allow_reuse_port and hasattr(socket, "SO_REUSEPORT")
and self.address_family in (socket.AF_INET, socket.AF_INET6)
):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
> self.socket.bind(self.server_address)
E OverflowError: bind(): port must be 0-65535.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/socketserver.py:478: OverflowError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:15:42.545995Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T20:15:42.546159Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:15:42.545995Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T20:15:42.546159Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_rejects_invalid_metrics_listener[127.0.0.1-65536-metrics_port must be between 1 and 65535] - OverflowError: bind(): port must be 0-65535.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 212 passed, 3 deselected, 1 warning in 3.95s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 15, definition_name: lifespan
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -159,7 +159,7 @@
metrics_port = int(settings.metrics_port)
if not metrics_host.strip():
raise RuntimeError("metrics_host must not be empty")
- if not 1 <= metrics_port <= 65535:
+ if not 1 <= metrics_port <= 65534:
raise RuntimeError("metrics_port must be between 1 and 65535")
if metrics_port == int(settings.port):
raise RuntimeError("metrics_port must differ from the public HTTP port")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 16, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -325,7 +325,7 @@
if raw:
# X-Forwarded-For may be a comma-separated list; the leftmost
# entry is the original client (proxies append to the right).
- client_ip = raw.split(",")[0].strip()
+ client_ip = raw.split(",")[ 1].strip()
if not client_ip and scope.get("client"):
client_ip = scope["client"][0]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........F
=================================== FAILURES ===================================
______________ test_logging_middleware_respects_configured_header ______________
def test_logging_middleware_respects_configured_header():
"""Test that logging_middleware uses the configured client_ip_header setting."""
from unittest.mock import patch
import structlog.contextvars
from stampbot.main import app
with patch("stampbot.main.settings") as mock_settings:
mock_settings.get = lambda key, default=None: (
"X-Real-IP" if key == "client_ip_header" else default
)
client = TestClient(app)
captured: dict[str, str | None] = {}
original_bind = structlog.contextvars.bind_contextvars
def capturing_bind(**kw: object) -> None:
captured.update({k: str(v) for k, v in kw.items()})
original_bind(**kw)
with patch("stampbot.main.structlog.contextvars.bind_contextvars", capturing_bind):
> client.get("/health", headers={"X-Real-IP": "198.51.100.7"})
tests/test_main.py:479:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:482: in get
return super().get(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1053: in get
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.main.LoggingMiddleware object at 0x7f891adf8440>
scope = {'type': 'http', 'http_version': '1.1', 'method': 'GET', 'path': '/health', ...}
receive = <function _TestClientTransport.handle_request.<locals>.receive at 0x7f891ad0ec40>
send = <function ServerErrorMiddleware.__call__.<locals>._send at 0x7f891ad0eae0>
async def __call__(self, scope: Any, receive: Any, send: Any) -> None:
"""Process an ASGI event, binding client IP to the structlog context.
For HTTP scopes the middleware clears any inherited structlog context,
extracts the real client IP from the configured forwarding header (or
the direct connection address as a fallback), and binds it so that
every log record emitted during the request includes ``client_ip``.
Non-HTTP scopes (lifespan, WebSocket) are passed through unchanged.
Args:
scope: ASGI connection scope.
receive: ASGI receive callable.
send: ASGI send callable.
"""
if scope["type"] == "http":
structlog.contextvars.clear_contextvars()
client_ip: str | None = None
client_ip_header: str = settings.get("client_ip_header", "X-Forwarded-For")
if client_ip_header:
raw = Request(scope).headers.get(client_ip_header)
if raw:
# X-Forwarded-For may be a comma-separated list; the leftmost
# entry is the original client (proxies append to the right).
> client_ip = raw.split(",")[ 1].strip()
^^^^^^^^^^^^^^^^^^
E IndexError: list index out of range
stampbot/main.py:328: IndexError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_logging_middleware_respects_configured_header - IndexError: list index out of range
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 224 passed, 3 deselected, 1 warning in 3.85s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 17, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -325,7 +325,7 @@
if raw:
# X-Forwarded-For may be a comma-separated list; the leftmost
# entry is the original client (proxies append to the right).
- client_ip = raw.split(",")[0].strip()
+ client_ip = raw.split(",")[ -1].strip()
if not client_ip and scope.get("client"):
client_ip = scope["client"][0]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 18, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -328,7 +328,7 @@
client_ip = raw.split(",")[0].strip()
if not client_ip and scope.get("client"):
- client_ip = scope["client"][0]
+ client_ip = scope["client"][ 1]
if client_ip:
structlog.contextvars.bind_contextvars(client_ip=client_ip)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 19, definition_name: __call__
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -328,7 +328,7 @@
client_ip = raw.split(",")[0].strip()
if not client_ip and scope.get("client"):
- client_ip = scope["client"][0]
+ client_ip = scope["client"][ -1]
if client_ip:
structlog.contextvars.bind_contextvars(client_ip=client_ip)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 20, definition_name: root
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -348,7 +348,7 @@
"""
configured = is_configured()
if not configured and _setup_is_available(configured=False):
- return RedirectResponse(url="/setup", status_code=307)
+ return RedirectResponse(url="/setup", status_code= 308)
return JSONResponse(
content={"app": "stampbot", "version": APP_VERSION, "status": "running"},........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...............F
=================================== FAILURES ===================================
_ TestSetupEndpointsUnconfigured.test_root_redirects_to_setup_when_unconfigured _
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7f7a55196490>
def test_root_redirects_to_setup_when_unconfigured(self):
"""Test root redirects to /setup when not configured."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/", follow_redirects=False)
> assert response.status_code == 307
E assert 308 == 307
E + where 308 = <Response [308 Permanent Redirect]>.status_code
tests/test_setup_endpoints.py:187: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ \"HTTP/1.1 308 Permanent Redirect\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:29:15.055039Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ "HTTP/1.1 308 Permanent Redirect"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_root_redirects_to_setup_when_unconfigured - assert 308 == 307
+ where 308 = <Response [308 Permanent Redirect]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 303 passed, 3 deselected, 1 warning in 3.81s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 21, definition_name: root
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -348,7 +348,7 @@
"""
configured = is_configured()
if not configured and _setup_is_available(configured=False):
- return RedirectResponse(url="/setup", status_code=307)
+ return RedirectResponse(url="/setup", status_code= 306)
return JSONResponse(
content={"app": "stampbot", "version": APP_VERSION, "status": "running"},........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...............F
=================================== FAILURES ===================================
_ TestSetupEndpointsUnconfigured.test_root_redirects_to_setup_when_unconfigured _
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7fbb4af7e490>
def test_root_redirects_to_setup_when_unconfigured(self):
"""Test root redirects to /setup when not configured."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
mock_settings.app_name = "stampbot"
mock_settings.host = "0.0.0.0"
mock_settings.port = 8000
mock_settings.log_level = "INFO"
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/", follow_redirects=False)
> assert response.status_code == 307
E assert 306 == 307
E + where 306 = <Response [306 ]>.status_code
tests/test_setup_endpoints.py:187: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ \"HTTP/1.1 306 \"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:52:43.105494Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ "HTTP/1.1 306 "
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_root_redirects_to_setup_when_unconfigured - assert 306 == 307
+ where 306 = <Response [306 ]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 303 passed, 3 deselected, 1 warning in 3.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 22, definition_name: ready
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -396,7 +396,7 @@
checks = {"configured": configured, "setup_enabled": setup_enabled}
is_ready = configured or setup_enabled
return JSONResponse(
- status_code=200 if is_ready else 503,
+ status_code= 201 if is_ready else 503,
content={"status": "ready" if is_ready else "not ready", "checks": checks},
)
........................................................................ [ 15%]
........................................................................ [ 31%]
...........................................................F
=================================== FAILURES ===================================
________________________ test_ready_endpoint_configured ________________________
test_client = <starlette.testclient.TestClient object at 0x7fa864fa3350>
def test_ready_endpoint_configured(test_client: TestClient):
"""Test readiness endpoint returns 200 when the app is configured."""
with patch("stampbot.main.is_configured", return_value=True):
response = test_client.get("/ready")
> assert response.status_code == 200
E assert 201 == 200
E + where 201 = <Response [201 Created]>.status_code
tests/test_main.py:102: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 201 Created\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:48:51.155266Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 201 Created"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_configured - assert 201 == 200
+ where 201 = <Response [201 Created]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 203 passed, 3 deselected, 1 warning in 3.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 23, definition_name: ready
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -396,7 +396,7 @@
checks = {"configured": configured, "setup_enabled": setup_enabled}
is_ready = configured or setup_enabled
return JSONResponse(
- status_code=200 if is_ready else 503,
+ status_code= 199 if is_ready else 503,
content={"status": "ready" if is_ready else "not ready", "checks": checks},
)
........................................................................ [ 15%]
........................................................................ [ 31%]
...........................................................F
=================================== FAILURES ===================================
________________________ test_ready_endpoint_configured ________________________
test_client = <starlette.testclient.TestClient object at 0x7f2543c9b350>
def test_ready_endpoint_configured(test_client: TestClient):
"""Test readiness endpoint returns 200 when the app is configured."""
with patch("stampbot.main.is_configured", return_value=True):
response = test_client.get("/ready")
> assert response.status_code == 200
E assert 199 == 200
E + where 199 = <Response [199 ]>.status_code
tests/test_main.py:102: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 199 \"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:02:56.997376Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 199 "
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_configured - assert 199 == 200
+ where 199 = <Response [199 ]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 203 passed, 3 deselected, 1 warning in 3.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 24, definition_name: ready
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -396,7 +396,7 @@
checks = {"configured": configured, "setup_enabled": setup_enabled}
is_ready = configured or setup_enabled
return JSONResponse(
- status_code=200 if is_ready else 503,
+ status_code=200 if is_ready else 504,
content={"status": "ready" if is_ready else "not ready", "checks": checks},
)
........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................................F
=================================== FAILURES ===================================
_______________ test_ready_endpoint_unconfigured_setup_disabled ________________
test_client = <starlette.testclient.TestClient object at 0x7fe0b0dbbf00>
def test_ready_endpoint_unconfigured_setup_disabled(test_client: TestClient):
"""Test readiness is 503 only when unconfigured and setup is disabled."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.get.side_effect = lambda key, default=None: {
"setup_enabled": False,
"setup_allow_configured": False,
}.get(key, default)
response = test_client.get("/ready")
> assert response.status_code == 503
E assert 504 == 503
E + where 504 = <Response [504 Gateway Timeout]>.status_code
tests/test_main.py:140: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 504 Gateway Timeout\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:01:57.799907Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 504 Gateway Timeout"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_unconfigured_setup_disabled - assert 504 == 503
+ where 504 = <Response [504 Gateway Timeout]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 205 passed, 3 deselected, 1 warning in 3.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 25, definition_name: ready
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -396,7 +396,7 @@
checks = {"configured": configured, "setup_enabled": setup_enabled}
is_ready = configured or setup_enabled
return JSONResponse(
- status_code=200 if is_ready else 503,
+ status_code=200 if is_ready else 502,
content={"status": "ready" if is_ready else "not ready", "checks": checks},
)
........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................................F
=================================== FAILURES ===================================
_______________ test_ready_endpoint_unconfigured_setup_disabled ________________
test_client = <starlette.testclient.TestClient object at 0x7f698979ff00>
def test_ready_endpoint_unconfigured_setup_disabled(test_client: TestClient):
"""Test readiness is 503 only when unconfigured and setup is disabled."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
mock_settings.get.side_effect = lambda key, default=None: {
"setup_enabled": False,
"setup_allow_configured": False,
}.get(key, default)
response = test_client.get("/ready")
> assert response.status_code == 503
E assert 502 == 503
E + where 502 = <Response [502 Bad Gateway]>.status_code
tests/test_main.py:140: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 502 Bad Gateway\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:09:46.192105Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 502 Bad Gateway"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_unconfigured_setup_disabled - assert 502 == 503
+ where 502 = <Response [502 Bad Gateway]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 205 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 26, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -449,7 +449,7 @@
# Check if app is configured
if not is_configured():
raise HTTPException(
- status_code=503,
+ status_code= 504,
detail="Stampbot not configured. Visit /setup to complete setup.",
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.....................F
=================================== FAILURES ===================================
__ TestSetupEndpointsUnconfigured.test_webhook_returns_503_when_unconfigured ___
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7f328e2888d0>
def test_webhook_returns_503_when_unconfigured(self):
"""Test webhook returns 503 when not configured."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.post("/webhook", json={})
> assert response.status_code == 503
E assert 504 == 503
E + where 504 = <Response [504 Gateway Timeout]>.status_code
tests/test_setup_endpoints.py:335: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 504 Gateway Timeout\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:27:34.061134Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 504 Gateway Timeout"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_webhook_returns_503_when_unconfigured - assert 504 == 503
+ where 504 = <Response [504 Gateway Timeout]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 309 passed, 3 deselected, 1 warning in 3.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 27, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -449,7 +449,7 @@
# Check if app is configured
if not is_configured():
raise HTTPException(
- status_code=503,
+ status_code= 502,
detail="Stampbot not configured. Visit /setup to complete setup.",
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.....................F
=================================== FAILURES ===================================
__ TestSetupEndpointsUnconfigured.test_webhook_returns_503_when_unconfigured ___
self = <tests.test_setup_endpoints.TestSetupEndpointsUnconfigured object at 0x7f84567b48d0>
def test_webhook_returns_503_when_unconfigured(self):
"""Test webhook returns 503 when not configured."""
with (
patch("stampbot.main.is_configured", return_value=False),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
from fastapi.testclient import TestClient
from stampbot.main import app
client = TestClient(app, raise_server_exceptions=False)
response = client.post("/webhook", json={})
> assert response.status_code == 503
E assert 502 == 503
E + where 502 = <Response [502 Bad Gateway]>.status_code
tests/test_setup_endpoints.py:335: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 502 Bad Gateway\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:04:49.574290Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 502 Bad Gateway"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsUnconfigured::test_webhook_returns_503_when_unconfigured - assert 502 == 503
+ where 502 = <Response [502 Bad Gateway]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 309 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 28, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -455,7 +455,7 @@
if not x_github_event:
errors_total.labels(error_type="missing_event").inc()
- raise HTTPException(status_code=400, detail="Missing X-GitHub-Event header")
+ raise HTTPException(status_code= 401, detail="Missing X-GitHub-Event header")
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")........................................................................ [ 15%]
........................................................................ [ 31%]
.......................................................................F [ 46%]
=================================== FAILURES ===================================
______________________ test_webhook_missing_event_header _______________________
test_client = <starlette.testclient.TestClient object at 0x7fa4151cc9e0>
def test_webhook_missing_event_header(test_client: TestClient):
"""Test webhook rejects requests without X-GitHub-Event header."""
response = test_client.post(
"/webhook",
json={"zen": "test"},
headers={"X-Hub-Signature-256": "sha256=invalid"},
)
> assert response.status_code == 400
E assert 401 == 400
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:285: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:20:46.457707Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_missing_event_header - assert 401 == 400
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 215 passed, 3 deselected, 1 warning in 3.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 29, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -455,7 +455,7 @@
if not x_github_event:
errors_total.labels(error_type="missing_event").inc()
- raise HTTPException(status_code=400, detail="Missing X-GitHub-Event header")
+ raise HTTPException(status_code= 399, detail="Missing X-GitHub-Event header")
# Check content length before reading body to prevent memory exhaustion
content_length = request.headers.get("content-length")........................................................................ [ 15%]
........................................................................ [ 31%]
.......................................................................F [ 46%]
=================================== FAILURES ===================================
______________________ test_webhook_missing_event_header _______________________
test_client = <starlette.testclient.TestClient object at 0x7f2e05c0c9e0>
def test_webhook_missing_event_header(test_client: TestClient):
"""Test webhook rejects requests without X-GitHub-Event header."""
response = test_client.post(
"/webhook",
json={"zen": "test"},
headers={"X-Hub-Signature-256": "sha256=invalid"},
)
> assert response.status_code == 400
E assert 399 == 400
E + where 399 = <Response [399 ]>.status_code
tests/test_main.py:285: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 399 \"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:21:55.091800Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 399 "
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_missing_event_header - assert 399 == 400
+ where 399 = <Response [399 ]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 215 passed, 3 deselected, 1 warning in 3.56s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 30, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -461,7 +461,7 @@
content_length = request.headers.get("content-length")
if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
- raise HTTPException(status_code=413, detail="Request body too large")
+ raise HTTPException(status_code= 414, detail="Request body too large")
# Get raw body for signature verification
body = await request.body()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...F
=================================== FAILURES ===================================
____________________ test_webhook_content_length_too_large _____________________
test_client = <starlette.testclient.TestClient object at 0x7f13b36bb680>
def test_webhook_content_length_too_large(test_client: TestClient):
"""Test webhook rejects requests with Content-Length exceeding limit."""
response = test_client.post(
"/webhook",
content=b"x", # Small actual body
headers={
"Content-Type": "application/json",
"Content-Length": "20000000", # 20MB - exceeds 10MB limit
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 414 == 413
E + where 414 = <Response [414 Request-URI Too Long]>.status_code
tests/test_main.py:378: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 414 Request-URI Too Long\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:01:04.674896Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 414 Request-URI Too Long"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_content_length_too_large - assert 414 == 413
+ where 414 = <Response [414 Request-URI Too Long]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 219 passed, 3 deselected, 1 warning in 3.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 31, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -461,7 +461,7 @@
content_length = request.headers.get("content-length")
if content_length and int(content_length) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
- raise HTTPException(status_code=413, detail="Request body too large")
+ raise HTTPException(status_code= 412, detail="Request body too large")
# Get raw body for signature verification
body = await request.body()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...F
=================================== FAILURES ===================================
____________________ test_webhook_content_length_too_large _____________________
test_client = <starlette.testclient.TestClient object at 0x7f8014943350>
def test_webhook_content_length_too_large(test_client: TestClient):
"""Test webhook rejects requests with Content-Length exceeding limit."""
response = test_client.post(
"/webhook",
content=b"x", # Small actual body
headers={
"Content-Type": "application/json",
"Content-Length": "20000000", # 20MB - exceeds 10MB limit
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 412 == 413
E + where 412 = <Response [412 Precondition Failed]>.status_code
tests/test_main.py:378: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 412 Precondition Failed\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:14:29.795142Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 412 Precondition Failed"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_content_length_too_large - assert 412 == 413
+ where 412 = <Response [412 Precondition Failed]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 219 passed, 3 deselected, 1 warning in 3.56s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 32, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -469,7 +469,7 @@
# Double-check actual body size (in case content-length was missing/incorrect)
if len(body) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
- raise HTTPException(status_code=413, detail="Request body too large")
+ raise HTTPException(status_code= 414, detail="Request body too large")
# Verify signature
if not webhook_handler.verify_signature(body, x_hub_signature_256):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....F
=================================== FAILURES ===================================
_________________________ test_webhook_body_too_large __________________________
test_client = <starlette.testclient.TestClient object at 0x7fd52b4b38a0>
def test_webhook_body_too_large(test_client: TestClient):
"""Test webhook rejects requests where actual body exceeds limit.
This tests the secondary body size check (lines 231-232) which catches
cases where Content-Length header is missing or incorrect.
"""
# Create a body larger than MAX_WEBHOOK_BODY_SIZE (10MB)
large_body = b"x" * (10 * 1024 * 1024 + 1) # 10MB + 1 byte
# Set Content-Length to a small value to bypass the header check (line 222)
# but the actual body size check (line 230) should still catch it
response = test_client.post(
"/webhook",
content=large_body,
headers={
"Content-Type": "application/json",
"Content-Length": "100", # Lie about size to bypass first check
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 414 == 413
E + where 414 = <Response [414 Request-URI Too Long]>.status_code
tests/test_main.py:403: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 414 Request-URI Too Long\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:04:28.959195Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 414 Request-URI Too Long"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_body_too_large - assert 414 == 413
+ where 414 = <Response [414 Request-URI Too Long]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 220 passed, 3 deselected, 1 warning in 3.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 33, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -469,7 +469,7 @@
# Double-check actual body size (in case content-length was missing/incorrect)
if len(body) > MAX_WEBHOOK_BODY_SIZE:
errors_total.labels(error_type="payload_too_large").inc()
- raise HTTPException(status_code=413, detail="Request body too large")
+ raise HTTPException(status_code= 412, detail="Request body too large")
# Verify signature
if not webhook_handler.verify_signature(body, x_hub_signature_256):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....F
=================================== FAILURES ===================================
_________________________ test_webhook_body_too_large __________________________
test_client = <starlette.testclient.TestClient object at 0x7f51a2fb38a0>
def test_webhook_body_too_large(test_client: TestClient):
"""Test webhook rejects requests where actual body exceeds limit.
This tests the secondary body size check (lines 231-232) which catches
cases where Content-Length header is missing or incorrect.
"""
# Create a body larger than MAX_WEBHOOK_BODY_SIZE (10MB)
large_body = b"x" * (10 * 1024 * 1024 + 1) # 10MB + 1 byte
# Set Content-Length to a small value to bypass the header check (line 222)
# but the actual body size check (line 230) should still catch it
response = test_client.post(
"/webhook",
content=large_body,
headers={
"Content-Type": "application/json",
"Content-Length": "100", # Lie about size to bypass first check
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=fake",
},
)
> assert response.status_code == 413
E assert 412 == 413
E + where 412 = <Response [412 Precondition Failed]>.status_code
tests/test_main.py:403: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 412 Precondition Failed\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:42:13.533623Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 412 Precondition Failed"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_body_too_large - assert 412 == 413
+ where 412 = <Response [412 Precondition Failed]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 220 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 34, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -476,7 +476,7 @@
webhook_signature_validations_total.labels(result="invalid").inc()
errors_total.labels(error_type="signature_invalid").inc()
logger.warning("Invalid webhook signature")
- raise HTTPException(status_code=401, detail="Invalid signature")
+ raise HTTPException(status_code= 402, detail="Invalid signature")
webhook_signature_validations_total.labels(result="valid").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fede36ab680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 402 == 401
E + where 402 = <Response [402 Payment Required]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T22:36:00.407012Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 402 Payment Required\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:36:00.407984Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T22:36:00.407012Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 402 Payment Required"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 402 == 401
+ where 402 = <Response [402 Payment Required]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.52s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 35, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -476,7 +476,7 @@
webhook_signature_validations_total.labels(result="invalid").inc()
errors_total.labels(error_type="signature_invalid").inc()
logger.warning("Invalid webhook signature")
- raise HTTPException(status_code=401, detail="Invalid signature")
+ raise HTTPException(status_code= 400, detail="Invalid signature")
webhook_signature_validations_total.labels(result="valid").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fe15c79f680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 400 == 401
E + where 400 = <Response [400 Bad Request]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T21:27:00.629426Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 400 Bad Request\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:27:00.630389Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T21:27:00.629426Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 400 Bad Request"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 400 == 401
+ where 400 = <Response [400 Bad Request]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 36, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -486,7 +486,7 @@
except Exception as e:
errors_total.labels(error_type="payload_invalid").inc()
logger.error("Failed to parse webhook payload: %s", e)
- raise HTTPException(status_code=400, detail="Invalid JSON payload") from None
+ raise HTTPException(status_code= 401, detail="Invalid JSON payload") from None
# The header is only trusted once the HMAC check has passed. Bound length
# keeps a malformed value out of log storage......................................................................... [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.F
=================================== FAILURES ===================================
__________________________ test_webhook_invalid_json ___________________________
test_client = <starlette.testclient.TestClient object at 0x7f07438ec270>
def test_webhook_invalid_json(test_client: TestClient):
"""Test webhook rejects invalid JSON payload."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
# Create a valid signature for invalid JSON
body = b"not valid json"
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 400
E assert 401 == 400
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:330: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Failed to parse webhook payload: Expecting value: line 1 column 1 (char 0)", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T21:12:16.295831Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:12:16.296644Z"}
------------------------------ Captured log call -------------------------------
ERROR stampbot.main:main.py:488 {'event': 'Failed to parse webhook payload: Expecting value: line 1 column 1 (char 0)', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T21:12:16.295831Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_invalid_json - assert 401 == 400
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 217 passed, 3 deselected, 1 warning in 3.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 37, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -486,7 +486,7 @@
except Exception as e:
errors_total.labels(error_type="payload_invalid").inc()
logger.error("Failed to parse webhook payload: %s", e)
- raise HTTPException(status_code=400, detail="Invalid JSON payload") from None
+ raise HTTPException(status_code= 399, detail="Invalid JSON payload") from None
# The header is only trusted once the HMAC check has passed. Bound length
# keeps a malformed value out of log storage......................................................................... [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.F
=================================== FAILURES ===================================
__________________________ test_webhook_invalid_json ___________________________
test_client = <starlette.testclient.TestClient object at 0x7fa7690fc270>
def test_webhook_invalid_json(test_client: TestClient):
"""Test webhook rejects invalid JSON payload."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
# Create a valid signature for invalid JSON
body = b"not valid json"
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 400
E assert 399 == 400
E + where 399 = <Response [399 ]>.status_code
tests/test_main.py:330: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Failed to parse webhook payload: Expecting value: line 1 column 1 (char 0)", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T21:00:30.527372Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 399 \"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:00:30.528182Z"}
------------------------------ Captured log call -------------------------------
ERROR stampbot.main:main.py:488 {'event': 'Failed to parse webhook payload: Expecting value: line 1 column 1 (char 0)', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T21:00:30.527372Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 399 "
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_invalid_json - assert 399 == 400
+ where 399 = <Response [399 ]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 217 passed, 3 deselected, 1 warning in 3.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 38, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -511,7 +511,7 @@
errors_total.labels(error_type="webhook_handler_error").inc()
sanitized = _sanitize_error(e)
logger.error("Error handling webhook event: %s", sanitized, extra={"error": sanitized})
- raise HTTPException(status_code=500, detail="Internal server error") from None
+ raise HTTPException(status_code= 501, detail="Internal server error") from None
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..........F
=================================== FAILURES ===================================
________________________ test_webhook_handler_exception ________________________
test_client = <starlette.testclient.TestClient object at 0x7f1fecad6690>
def test_webhook_handler_exception(test_client: TestClient):
"""Test webhook returns 500 when handler raises exception."""
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "test"}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
with patch("stampbot.main.webhook_handler.handle_event") as mock_handle:
mock_handle.side_effect = Exception("Unexpected error")
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 500
E assert 501 == 500
E + where 501 = <Response [501 Not Implemented]>.status_code
tests/test_main.py:545: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"error": "Unexpected error"}, "event": "Error handling webhook event: Unexpected error", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T21:25:19.275891Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 501 Not Implemented\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:25:19.276729Z"}
------------------------------ Captured log call -------------------------------
ERROR stampbot.main:main.py:513 {'extra': {'error': 'Unexpected error'}, 'event': 'Error handling webhook event: Unexpected error', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T21:25:19.275891Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 501 Not Implemented"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_handler_exception - assert 501 == 500
+ where 501 = <Response [501 Not Implemented]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 226 passed, 3 deselected, 1 warning in 3.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 39, definition_name: webhook
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -511,7 +511,7 @@
errors_total.labels(error_type="webhook_handler_error").inc()
sanitized = _sanitize_error(e)
logger.error("Error handling webhook event: %s", sanitized, extra={"error": sanitized})
- raise HTTPException(status_code=500, detail="Internal server error") from None
+ raise HTTPException(status_code= 499, detail="Internal server error") from None
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..........F
=================================== FAILURES ===================================
________________________ test_webhook_handler_exception ________________________
test_client = <starlette.testclient.TestClient object at 0x7f097d6d2690>
def test_webhook_handler_exception(test_client: TestClient):
"""Test webhook returns 500 when handler raises exception."""
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "test"}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
with patch("stampbot.main.webhook_handler.handle_event") as mock_handle:
mock_handle.side_effect = Exception("Unexpected error")
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 500
E assert 499 == 500
E + where 499 = <Response [499 ]>.status_code
tests/test_main.py:545: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"error": "Unexpected error"}, "event": "Error handling webhook event: Unexpected error", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T20:55:04.237248Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 499 \"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:55:04.238078Z"}
------------------------------ Captured log call -------------------------------
ERROR stampbot.main:main.py:513 {'extra': {'error': 'Unexpected error'}, 'event': 'Error handling webhook event: Unexpected error', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T20:55:04.237248Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 499 "
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_handler_exception - assert 499 == 500
+ where 499 = <Response [499 ]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 226 passed, 3 deselected, 1 warning in 3.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 40, definition_name: setup_callback
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -617,7 +617,7 @@
credentials = await exchange_code_for_credentials(code)
except Exception as e:
logger.error("Failed to exchange code for credentials: %s", _sanitize_error(e))
- raise HTTPException(status_code=500, detail="Failed to complete setup") from None
+ raise HTTPException(status_code= 501, detail="Failed to complete setup") from None
# Security note: Credentials are displayed in the HTML response for user convenience.
# This mirrors GitHub's own manifest flow behavior. The tradeoffs are acceptable because:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................F
=================================== FAILURES ===================================
____________ TestSetupCallback.test_callback_handles_exchange_error ____________
self = <tests.test_setup_endpoints.TestSetupCallback object at 0x7fc1d21a15b0>
test_client = <starlette.testclient.TestClient object at 0x7fc1d1cbdbf0>
def test_callback_handles_exchange_error(self, test_client):
"""Test callback handles exchange error gracefully."""
with (
patch("stampbot.main._require_setup_access"),
patch(
"stampbot.main.exchange_code_for_credentials",
new_callable=AsyncMock,
side_effect=Exception("API error"),
),
):
response = test_client.get("/setup/callback?code=bad-code")
> assert response.status_code == 500
E assert 501 == 500
E + where 501 = <Response [501 Not Implemented]>.status_code
tests/test_setup_endpoints.py:406: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Failed to exchange code for credentials: API error", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T20:34:02.450008Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: GET http://testserver/setup/callback?code=bad-code \"HTTP/1.1 501 Not Implemented\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:34:02.450993Z"}
------------------------------ Captured log call -------------------------------
ERROR stampbot.main:main.py:619 {'event': 'Failed to exchange code for credentials: API error', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T20:34:02.450008Z'}
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup/callback?code=bad-code "HTTP/1.1 501 Not Implemented"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupCallback::test_callback_handles_exchange_error - assert 501 == 500
+ where 501 = <Response [501 Not Implemented]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 312 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 41, definition_name: setup_callback
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -617,7 +617,7 @@
credentials = await exchange_code_for_credentials(code)
except Exception as e:
logger.error("Failed to exchange code for credentials: %s", _sanitize_error(e))
- raise HTTPException(status_code=500, detail="Failed to complete setup") from None
+ raise HTTPException(status_code= 499, detail="Failed to complete setup") from None
# Security note: Credentials are displayed in the HTML response for user convenience.
# This mirrors GitHub's own manifest flow behavior. The tradeoffs are acceptable because:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................F
=================================== FAILURES ===================================
____________ TestSetupCallback.test_callback_handles_exchange_error ____________
self = <tests.test_setup_endpoints.TestSetupCallback object at 0x7f79b6c915b0>
test_client = <starlette.testclient.TestClient object at 0x7f79b68be140>
def test_callback_handles_exchange_error(self, test_client):
"""Test callback handles exchange error gracefully."""
with (
patch("stampbot.main._require_setup_access"),
patch(
"stampbot.main.exchange_code_for_credentials",
new_callable=AsyncMock,
side_effect=Exception("API error"),
),
):
response = test_client.get("/setup/callback?code=bad-code")
> assert response.status_code == 500
E assert 499 == 500
E + where 499 = <Response [499 ]>.status_code
tests/test_setup_endpoints.py:406: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Failed to exchange code for credentials: API error", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T22:40:31.826049Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: GET http://testserver/setup/callback?code=bad-code \"HTTP/1.1 499 \"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:40:31.826966Z"}
------------------------------ Captured log call -------------------------------
ERROR stampbot.main:main.py:619 {'event': 'Failed to exchange code for credentials: API error', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T22:40:31.826049Z'}
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup/callback?code=bad-code "HTTP/1.1 499 "
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupCallback::test_callback_handles_exchange_error - assert 499 == 500
+ where 499 = <Response [499 ]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 312 passed, 3 deselected, 1 warning in 3.95s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/RemoveDecorator, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -119,9 +119,6 @@
status_code=503,
detail=("Setup requires STAMPBOT_BASE_URL to be set to Stampbot's trusted public URL"),
) from None
-
-
-@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application lifespan manager for startup and shutdown events.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 1, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -226,9 +226,6 @@
partial_match = route_path
return partial_match or UNMATCHED_ENDPOINT
-
-
-@app.middleware("http")
async def metrics_middleware(request: Request, call_next: Any) -> Response:
"""Middleware to track HTTP metrics.
........................................................................ [ 15%]
........................................................................ [ 31%]
................................F
=================================== FAILURES ===================================
__________________ test_unknown_paths_share_one_bounded_label __________________
test_client = <starlette.testclient.TestClient object at 0x7f6e40e1a660>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140111514197968'>, 'http_request_duration_seconds': ...='140111514196624'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140111514195952'>, ...}
def test_unknown_paths_share_one_bounded_label(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Arbitrary 404 paths must not create attacker-controlled label values."""
unknown_paths = (
"/not-a-route/attacker-value-one",
"/another/made-up/path/attacker-value-two",
)
for path in unknown_paths:
response = test_client.post(path, content=b"request body")
assert response.status_code == 404
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == [
UNMATCHED_ENDPOINT,
UNMATCHED_ENDPOINT,
]
E AssertionError: assert [] == ['unmatched', 'unmatched']
E
E Right contains 2 more items, first extra item: 'unmatched'
E
E Full diff:
E + []
E - [
E - 'unmatched',
E - 'unmatched',
E - ]
tests/test_http_metrics.py:101: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:33:50.590189Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T21:33:50.591759Z[0m [[32m[1minfo [0m] [1mHTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/not-a-route/attacker-value-one "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/another/made-up/path/attacker-value-two "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_unknown_paths_share_one_bounded_label - AssertionError: assert [] == ['unmatched', 'unmatched']
Right contains 2 more items, first extra item: 'unmatched'
Full diff:
+ []
- [
- 'unmatched',
- 'unmatched',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 176 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 2, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -337,9 +337,6 @@
app.add_middleware(LoggingMiddleware)
-
-
-@app.get("/")
async def root() -> Response:
"""Root endpoint - redirects to setup if not configured.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................F
=================================== FAILURES ===================================
______________________________ test_root_endpoint ______________________________
test_client = <starlette.testclient.TestClient object at 0x7fe7f0fa7130>
def test_root_endpoint(test_client: TestClient):
"""Test root endpoint returns basic info."""
from stampbot.version import APP_VERSION
response = test_client.get("/")
> assert response.status_code == 200
E assert 404 == 200
E + where 404 = <Response [404 Not Found]>.status_code
tests/test_main.py:73: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ \"HTTP/1.1 404 Not Found\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:14:38.886346Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_root_endpoint - assert 404 == 200
+ where 404 = <Response [404 Not Found]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 200 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 3, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -353,9 +353,6 @@
return JSONResponse(
content={"app": "stampbot", "version": APP_VERSION, "status": "running"},
)
-
-
-@app.get("/health")
async def health() -> dict[str, str]:
"""Liveness check endpoint.
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................F
=================================== FAILURES ===================================
__________________ test_lifespan_startup_shutdown_configured ___________________
def test_lifespan_startup_shutdown_configured():
"""Test app lifespan startup and shutdown when configured."""
from stampbot.main import app
# Use TestClient as context manager to trigger lifespan events
with TestClient(app) as client:
# App should be running
response = client.get("/health")
> assert response.status_code == 200
E assert 404 == 200
E + where 404 = <Response [404 Not Found]>.status_code
tests/test_main.py:24: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"host": "0.0.0.0", "port": 8000, "log_level": "INFO"}, "event": "Starting stampbot on 0.0.0.0:8000", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:11:47.373372Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "GitHub App credentials configured successfully", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:11:47.373573Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
{"event": "HTTP Request: GET http://testserver/health \"HTTP/1.1 404 Not Found\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:11:47.374804Z"}
{"event": "Shutting down stampbot", "logger": "stampbot.main", "level": "info", "timestamp": "2026-09-04T22:11:47.375327Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.main:main.py:137 {'extra': {'host': '0.0.0.0', 'port': 8000, 'log_level': 'INFO'}, 'event': 'Starting stampbot on 0.0.0.0:8000', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:11:47.373372Z'}
INFO stampbot.main:main.py:155 {'event': 'GitHub App credentials configured successfully', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:11:47.373573Z'}
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/health "HTTP/1.1 404 Not Found"
INFO stampbot.main:main.py:178 {'event': 'Shutting down stampbot', 'logger': 'stampbot.main', 'level': 'info', 'timestamp': '2026-09-04T22:11:47.375327Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_lifespan_startup_shutdown_configured - assert 404 == 200
+ where 404 = <Response [404 Not Found]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 197 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 4, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -368,9 +368,6 @@
Dictionary with health status.
"""
return {"status": "healthy"}
-
-
-@app.get("/ready")
async def ready() -> Response:
"""Readiness check endpoint.
........................................................................ [ 15%]
........................................................................ [ 31%]
...........................................................F
=================================== FAILURES ===================================
________________________ test_ready_endpoint_configured ________________________
test_client = <starlette.testclient.TestClient object at 0x7f39813a7350>
def test_ready_endpoint_configured(test_client: TestClient):
"""Test readiness endpoint returns 200 when the app is configured."""
with patch("stampbot.main.is_configured", return_value=True):
response = test_client.get("/ready")
> assert response.status_code == 200
E assert 404 == 200
E + where 404 = <Response [404 Not Found]>.status_code
tests/test_main.py:102: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/ready \"HTTP/1.1 404 Not Found\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:44:38.212324Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/ready "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_ready_endpoint_configured - assert 404 == 200
+ where 404 = <Response [404 Not Found]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 203 passed, 3 deselected, 1 warning in 3.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 5, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -420,9 +420,6 @@
if len(raw) > MAX_DELIVERY_ID_LENGTH or not DELIVERY_ID_PATTERN.fullmatch(raw):
return None
return raw
-
-
-@app.post("/webhook")
async def webhook(
request: Request,
x_github_event: str = Header(None, alias="X-GitHub-Event"),........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f10ad0cebe0>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
# Will fail signature validation, but Content-Length tracking happens before that
> assert response.status_code == 401
E assert 404 == 401
E + where 404 = <Response [404 Not Found]>.status_code
tests/test_main.py:264: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 404 Not Found\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:58:39.949482Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - assert 404 == 401
+ where 404 = <Response [404 Not Found]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 3.56s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 6, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -512,14 +512,6 @@
sanitized = _sanitize_error(e)
logger.error("Error handling webhook event: %s", sanitized, extra={"error": sanitized})
raise HTTPException(status_code=500, detail="Internal server error") from None
-
-
-# =============================================================================
-# Setup Endpoints - GitHub App Manifest Flow
-# =============================================================================
-
-
-@app.get("/setup")
async def setup_page() -> Response:
"""Setup page with manifest creation button.
........................................................................ [ 15%]
........................................................................ [ 31%]
...................................F
=================================== FAILURES ===================================
________________ test_setup_routes_keep_distinct_static_labels _________________
test_client = <starlette.testclient.TestClient object at 0x7f551ddce780>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140003545585648'>, 'http_request_duration_seconds': ...='140003545586320'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140003545586656'>, ...}
def test_setup_routes_keep_distinct_static_labels(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Setup and callback traffic remains distinguishable by route."""
setup_response = test_client.get("/setup")
callback_response = test_client.get("/setup/callback")
> assert setup_response.status_code == 403
E assert 404 == 403
E + where 404 = <Response [404 Not Found]>.status_code
tests/test_http_metrics.py:143: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:33:09.676365Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/setup "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T20:33:09.679073Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/setup/callback "HTTP/1.1 422 Unprocessable Entity"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 404 Not Found"
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup/callback "HTTP/1.1 422 Unprocessable Entity"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_setup_routes_keep_distinct_static_labels - assert 404 == 403
+ where 404 = <Response [404 Not Found]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 179 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 7, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -596,9 +596,6 @@
"""
return HTMLResponse(content=html_content, headers=SETUP_HTML_HEADERS)
-
-
-@app.get("/setup/callback")
async def setup_callback(code: str) -> Response:
"""Handle callback from GitHub after app creation.
........................................................................ [ 15%]
........................................................................ [ 31%]
...................................F
=================================== FAILURES ===================================
________________ test_setup_routes_keep_distinct_static_labels _________________
test_client = <starlette.testclient.TestClient object at 0x7f456acee780>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='139936118269536'>, 'http_request_duration_seconds': ...='139936118270208'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='139936118270544'>, ...}
def test_setup_routes_keep_distinct_static_labels(
test_client: TestClient,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Setup and callback traffic remains distinguishable by route."""
setup_response = test_client.get("/setup")
callback_response = test_client.get("/setup/callback")
assert setup_response.status_code == 403
> assert callback_response.status_code == 422
E assert 404 == 422
E + where 404 = <Response [404 Not Found]>.status_code
tests/test_http_metrics.py:144: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:23:58.383505Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/setup "HTTP/1.1 403 Forbidden"[0m [[0m[1m[34mhttpx[0m][0m
[2m2026-09-04T20:23:58.385247Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/setup/callback "HTTP/1.1 404 Not Found"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup "HTTP/1.1 403 Forbidden"
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup/callback "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_setup_routes_keep_distinct_static_labels - assert 404 == 422
+ where 404 = <Response [404 Not Found]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 179 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 8, definition_name: None
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -714,9 +714,6 @@
"""
return HTMLResponse(content=html_content, headers=SETUP_HTML_HEADERS)
-
-
-@app.get("/setup/status")
async def setup_status() -> dict[str, Any]:
"""Check setup status.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.............F
=================================== FAILURES ===================================
_ TestSetupEndpointsConfigured.test_setup_status_is_closed_and_does_not_disclose_app_id _
self = <tests.test_setup_endpoints.TestSetupEndpointsConfigured object at 0x7f220bc908a0>
def test_setup_status_is_closed_and_does_not_disclose_app_id(self):
"""Test setup status follows the configured-state gate."""
with (
patch("stampbot.main.is_configured", return_value=True),
patch("stampbot.main.settings") as mock_settings,
):
configure_setup_settings(mock_settings, enabled=True)
from fastapi.testclient import TestClient
from stampbot.main import app
response = TestClient(app, raise_server_exceptions=False).get("/setup/status")
> assert response.status_code == 403
E assert 404 == 403
E + where 404 = <Response [404 Not Found]>.status_code
tests/test_setup_endpoints.py:138: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "HTTP Request: GET http://testserver/setup/status \"HTTP/1.1 404 Not Found\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:35:08.629957Z"}
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/setup/status "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_setup_endpoints.py::TestSetupEndpointsConfigured::test_setup_status_is_closed_and_does_not_disclose_app_id - assert 404 == 403
+ where 404 = <Response [404 Not Found]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 301 passed, 3 deselected, 1 warning in 3.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 0, definition_name: _metric_endpoint_label
--- mutation diff ---
--- a/stampbot/main.py
+++ b/stampbot/main.py
@@ -213,7 +213,7 @@
return current_path
partial_match: str | None = None
- for route in request.app.routes:
+ for route in []:
match, child_scope = route.matches(request.scope)
matched_route = child_scope.get("route", route)
route_path = getattr(matched_route, "path", None)........................................................................ [ 15%]
........................................................................ [ 31%]
..................................F
=================================== FAILURES ===================================
____________________ test_dynamic_path_uses_route_template _____________________
routed_app = <fastapi.applications.FastAPI object at 0x7fd17fd44ad0>
http_metric_spies = {'http_requests_total': <MagicMock name='http_requests_total' id='140537769512304'>, 'http_request_duration_seconds': ...='140537769512976'>, 'http_response_size_bytes': <MagicMock name='http_response_size_bytes' id='140537769511968'>, ...}
def test_dynamic_path_uses_route_template(
routed_app: FastAPI,
http_metric_spies: dict[str, MagicMock],
) -> None:
"""Path parameters are represented by their stable route template."""
client = TestClient(routed_app)
response = client.get("/widgets/user-controlled-123")
assert response.status_code == 200
> assert _endpoint_labels(http_metric_spies["http_requests_total"]) == ["/widgets/{widget_id}"]
E AssertionError: assert ['unmatched'] == ['/widgets/{widget_id}']
E
E At index 0 diff: 'unmatched' != '/widgets/{widget_id}'
E
E Full diff:
E [
E - '/widgets/{widget_id}',
E + 'unmatched',
E ]
tests/test_http_metrics.py:130: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:49:47.969983Z[0m [[32m[1minfo [0m] [1mHTTP Request: GET http://testserver/widgets/user-controlled-123 "HTTP/1.1 200 OK"[0m [[0m[1m[34mhttpx[0m][0m
------------------------------ Captured log call -------------------------------
INFO httpx:_client.py:1025 HTTP Request: GET http://testserver/widgets/user-controlled-123 "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_http_metrics.py::test_dynamic_path_uses_route_template - AssertionError: assert ['unmatched'] == ['/widgets/{widget_id}']
At index 0 diff: 'unmatched' != '/widgets/{widget_id}'
Full diff:
[
- '/widgets/{widget_id}',
+ 'unmatched',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 178 passed, 3 deselected, 1 warning in 2.97s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() + start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() * start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() / start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.97s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() // start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() % start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() ** start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_token_exchanges_once_and_refreshes_when_expired _
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7f3e3671ccd0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f3e35d317f0>
span = <MagicMock name='create_span' id='139905167733968'>
def test_token_exchanges_once_and_refreshes_when_expired(self, installation_auth, span):
"""Test PyGithub's ``token`` routes first exchange and refresh through the override."""
successes = _counter("success")
durations = _duration_count()
with patch.object(
Auth.AppInstallationAuth,
EXCHANGE,
side_effect=[_authorization(expired=True), _authorization(expired=False)],
) as exchange:
> assert installation_auth.token == TEST_TOKEN # first exchange
^^^^^^^^^^^^^^^^^^^^^^^
tests/test_installation_auth.py:67:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/installation_auth.py:57: in token
return super().token
^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/github/Auth.py:376: in token
self.__installation_authorization = self._get_installation_authorization()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f3e35d317f0>
def _get_installation_authorization(self) -> InstallationAuthorization:
"""Exchange the App JWT for an installation token, with a span and metrics.
Returns:
The new installation token and its expiry.
Raises:
github.GithubException: If GitHub rejects the exchange.
"""
start_time = time.time()
with create_span(
"github.get_installation_token",
{
"github.installation_id": self.installation_id,
"github.token_refresh": self._exchanged,
},
) as span:
try:
authorization = super()._get_installation_authorization()
except Exception as e:
github_api_requests_total.labels(operation="get_token", status="failure").inc()
set_span_error(span, e)
raise
else:
github_api_requests_total.labels(operation="get_token", status="success").inc()
set_span_ok(span)
self._exchanged = True
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
> time.time() ** start_time
)
E OverflowError: (34, 'Numerical result out of range')
stampbot/installation_auth.py:89: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_token_exchanges_once_and_refreshes_when_expired - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 182 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() >> start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_token_exchanges_once_and_refreshes_when_expired _
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7f5119b2ccd0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f511918d7f0>
span = <MagicMock name='create_span' id='139986290144464'>
def test_token_exchanges_once_and_refreshes_when_expired(self, installation_auth, span):
"""Test PyGithub's ``token`` routes first exchange and refresh through the override."""
successes = _counter("success")
durations = _duration_count()
with patch.object(
Auth.AppInstallationAuth,
EXCHANGE,
side_effect=[_authorization(expired=True), _authorization(expired=False)],
) as exchange:
> assert installation_auth.token == TEST_TOKEN # first exchange
^^^^^^^^^^^^^^^^^^^^^^^
tests/test_installation_auth.py:67:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/installation_auth.py:57: in token
return super().token
^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/github/Auth.py:376: in token
self.__installation_authorization = self._get_installation_authorization()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f511918d7f0>
def _get_installation_authorization(self) -> InstallationAuthorization:
"""Exchange the App JWT for an installation token, with a span and metrics.
Returns:
The new installation token and its expiry.
Raises:
github.GithubException: If GitHub rejects the exchange.
"""
start_time = time.time()
with create_span(
"github.get_installation_token",
{
"github.installation_id": self.installation_id,
"github.token_refresh": self._exchanged,
},
) as span:
try:
authorization = super()._get_installation_authorization()
except Exception as e:
github_api_requests_total.labels(operation="get_token", status="failure").inc()
set_span_error(span, e)
raise
else:
github_api_requests_total.labels(operation="get_token", status="success").inc()
set_span_ok(span)
self._exchanged = True
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
> time.time() >> start_time
)
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/installation_auth.py:89: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_token_exchanges_once_and_refreshes_when_expired - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 182 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() << start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_token_exchanges_once_and_refreshes_when_expired _
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7f0cce528cd0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f0ccdb7d7f0>
span = <MagicMock name='create_span' id='139692967720144'>
def test_token_exchanges_once_and_refreshes_when_expired(self, installation_auth, span):
"""Test PyGithub's ``token`` routes first exchange and refresh through the override."""
successes = _counter("success")
durations = _duration_count()
with patch.object(
Auth.AppInstallationAuth,
EXCHANGE,
side_effect=[_authorization(expired=True), _authorization(expired=False)],
) as exchange:
> assert installation_auth.token == TEST_TOKEN # first exchange
^^^^^^^^^^^^^^^^^^^^^^^
tests/test_installation_auth.py:67:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/installation_auth.py:57: in token
return super().token
^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/github/Auth.py:376: in token
self.__installation_authorization = self._get_installation_authorization()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f0ccdb7d7f0>
def _get_installation_authorization(self) -> InstallationAuthorization:
"""Exchange the App JWT for an installation token, with a span and metrics.
Returns:
The new installation token and its expiry.
Raises:
github.GithubException: If GitHub rejects the exchange.
"""
start_time = time.time()
with create_span(
"github.get_installation_token",
{
"github.installation_id": self.installation_id,
"github.token_refresh": self._exchanged,
},
) as span:
try:
authorization = super()._get_installation_authorization()
except Exception as e:
github_api_requests_total.labels(operation="get_token", status="failure").inc()
set_span_error(span, e)
raise
else:
github_api_requests_total.labels(operation="get_token", status="success").inc()
set_span_ok(span)
self._exchanged = True
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
> time.time() << start_time
)
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/installation_auth.py:89: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_token_exchanges_once_and_refreshes_when_expired - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 182 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() | start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_token_exchanges_once_and_refreshes_when_expired _
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7f6a9080ccd0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f6a903ed7f0>
span = <MagicMock name='create_span' id='140095663305936'>
def test_token_exchanges_once_and_refreshes_when_expired(self, installation_auth, span):
"""Test PyGithub's ``token`` routes first exchange and refresh through the override."""
successes = _counter("success")
durations = _duration_count()
with patch.object(
Auth.AppInstallationAuth,
EXCHANGE,
side_effect=[_authorization(expired=True), _authorization(expired=False)],
) as exchange:
> assert installation_auth.token == TEST_TOKEN # first exchange
^^^^^^^^^^^^^^^^^^^^^^^
tests/test_installation_auth.py:67:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/installation_auth.py:57: in token
return super().token
^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/github/Auth.py:376: in token
self.__installation_authorization = self._get_installation_authorization()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f6a903ed7f0>
def _get_installation_authorization(self) -> InstallationAuthorization:
"""Exchange the App JWT for an installation token, with a span and metrics.
Returns:
The new installation token and its expiry.
Raises:
github.GithubException: If GitHub rejects the exchange.
"""
start_time = time.time()
with create_span(
"github.get_installation_token",
{
"github.installation_id": self.installation_id,
"github.token_refresh": self._exchanged,
},
) as span:
try:
authorization = super()._get_installation_authorization()
except Exception as e:
github_api_requests_total.labels(operation="get_token", status="failure").inc()
set_span_error(span, e)
raise
else:
github_api_requests_total.labels(operation="get_token", status="success").inc()
set_span_ok(span)
self._exchanged = True
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
> time.time() | start_time
)
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/installation_auth.py:89: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_token_exchanges_once_and_refreshes_when_expired - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 182 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() & start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_token_exchanges_once_and_refreshes_when_expired _
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7f9dbf41ccd0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f9dbeff97f0>
span = <MagicMock name='create_span' id='140315491022032'>
def test_token_exchanges_once_and_refreshes_when_expired(self, installation_auth, span):
"""Test PyGithub's ``token`` routes first exchange and refresh through the override."""
successes = _counter("success")
durations = _duration_count()
with patch.object(
Auth.AppInstallationAuth,
EXCHANGE,
side_effect=[_authorization(expired=True), _authorization(expired=False)],
) as exchange:
> assert installation_auth.token == TEST_TOKEN # first exchange
^^^^^^^^^^^^^^^^^^^^^^^
tests/test_installation_auth.py:67:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/installation_auth.py:57: in token
return super().token
^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/github/Auth.py:376: in token
self.__installation_authorization = self._get_installation_authorization()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f9dbeff97f0>
def _get_installation_authorization(self) -> InstallationAuthorization:
"""Exchange the App JWT for an installation token, with a span and metrics.
Returns:
The new installation token and its expiry.
Raises:
github.GithubException: If GitHub rejects the exchange.
"""
start_time = time.time()
with create_span(
"github.get_installation_token",
{
"github.installation_id": self.installation_id,
"github.token_refresh": self._exchanged,
},
) as span:
try:
authorization = super()._get_installation_authorization()
except Exception as e:
github_api_requests_total.labels(operation="get_token", status="failure").inc()
set_span_error(span, e)
raise
else:
github_api_requests_total.labels(operation="get_token", status="success").inc()
set_span_ok(span)
self._exchanged = True
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
> time.time() & start_time
)
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/installation_auth.py:89: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_token_exchanges_once_and_refreshes_when_expired - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 182 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -86,6 +86,6 @@
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
- time.time() - start_time
+ time.time() ^ start_time
)
........................................................................ [ 15%]
........................................................................ [ 31%]
......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_token_exchanges_once_and_refreshes_when_expired _
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7fee04114cd0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7fedffbf17f0>
span = <MagicMock name='create_span' id='140660174697680'>
def test_token_exchanges_once_and_refreshes_when_expired(self, installation_auth, span):
"""Test PyGithub's ``token`` routes first exchange and refresh through the override."""
successes = _counter("success")
durations = _duration_count()
with patch.object(
Auth.AppInstallationAuth,
EXCHANGE,
side_effect=[_authorization(expired=True), _authorization(expired=False)],
) as exchange:
> assert installation_auth.token == TEST_TOKEN # first exchange
^^^^^^^^^^^^^^^^^^^^^^^
tests/test_installation_auth.py:67:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/installation_auth.py:57: in token
return super().token
^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/github/Auth.py:376: in token
self.__installation_authorization = self._get_installation_authorization()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7fedffbf17f0>
def _get_installation_authorization(self) -> InstallationAuthorization:
"""Exchange the App JWT for an installation token, with a span and metrics.
Returns:
The new installation token and its expiry.
Raises:
github.GithubException: If GitHub rejects the exchange.
"""
start_time = time.time()
with create_span(
"github.get_installation_token",
{
"github.installation_id": self.installation_id,
"github.token_refresh": self._exchanged,
},
) as span:
try:
authorization = super()._get_installation_authorization()
except Exception as e:
github_api_requests_total.labels(operation="get_token", status="failure").inc()
set_span_error(span, e)
raise
else:
github_api_requests_total.labels(operation="get_token", status="success").inc()
set_span_ok(span)
self._exchanged = True
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(
> time.time() ^ start_time
)
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/installation_auth.py:89: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_token_exchanges_once_and_refreshes_when_expired - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 182 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -82,7 +82,7 @@
else:
github_api_requests_total.labels(operation="get_token", status="success").inc()
set_span_ok(span)
- self._exchanged = True
+ self._exchanged = False
return authorization
finally:
github_api_request_duration_seconds.labels(operation="get_token").observe(........................................................................ [ 15%]
........................................................................ [ 31%]
......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_token_exchanges_once_and_refreshes_when_expired _
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7f9c20834cd0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f9c1bd697f0>
span = <MagicMock name='create_span' id='140308458681552'>
def test_token_exchanges_once_and_refreshes_when_expired(self, installation_auth, span):
"""Test PyGithub's ``token`` routes first exchange and refresh through the override."""
successes = _counter("success")
durations = _duration_count()
with patch.object(
Auth.AppInstallationAuth,
EXCHANGE,
side_effect=[_authorization(expired=True), _authorization(expired=False)],
) as exchange:
assert installation_auth.token == TEST_TOKEN # first exchange
assert installation_auth.token == TEST_TOKEN # expired, so refresh
assert installation_auth.token == TEST_TOKEN # fresh, no exchange
assert exchange.call_count == 2
assert _counter("success") == successes + 2
assert _duration_count() == durations + 2
> assert [c.args for c in span.call_args_list] == [
(
"github.get_installation_token",
{"github.installation_id": 777, "github.token_refresh": False},
),
(
"github.get_installation_token",
{"github.installation_id": 777, "github.token_refresh": True},
),
]
E AssertionError: assert [('github.get...esh': False})] == [('github.get...resh': True})]
E
E At index 1 diff: ('github.get_installation_token', {'github.installation_id': 777, 'github.token_refresh': False}) != ('github.get_installation_token', {'github.installation_id': 777, 'github.token_refresh': True})
E
E Full diff:
E [
E (
E 'github.get_installation_token',
E {
E 'github.installation_id': 777,
E 'github.token_refresh': False,
E },
E ),
E (
E 'github.get_installation_token',
E {
E 'github.installation_id': 777,
E - 'github.token_refresh': True,
E ? ^^^
E + 'github.token_refresh': False,
E ? ^^^^
E },
E ),
E ]
tests/test_installation_auth.py:74: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_token_exchanges_once_and_refreshes_when_expired - AssertionError: assert [('github.get...esh': False})] == [('github.get...resh': True})]
At index 1 diff: ('github.get_installation_token', {'github.installation_id': 777, 'github.token_refresh': False}) != ('github.get_installation_token', {'github.installation_id': 777, 'github.token_refresh': True})
Full diff:
[
(
'github.get_installation_token',
{
'github.installation_id': 777,
'github.token_refresh': False,
},
),
(
'github.get_installation_token',
{
'github.installation_id': 777,
- 'github.token_refresh': True,
? ^^^
+ 'github.token_refresh': False,
? ^^^^
},
),
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 182 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 0, definition_name: __init__
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -41,7 +41,7 @@
"""
super().__init__(app_auth, installation_id)
self._exchange_lock = threading.Lock()
- self._exchanged = False
+ self._exchanged = True
@property
def token(self) -> str:........................................................................ [ 15%]
........................................................................ [ 31%]
......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_token_exchanges_once_and_refreshes_when_expired _
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7f3e9b930cd0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7f3e9af757f0>
span = <MagicMock name='create_span' id='139906864608464'>
def test_token_exchanges_once_and_refreshes_when_expired(self, installation_auth, span):
"""Test PyGithub's ``token`` routes first exchange and refresh through the override."""
successes = _counter("success")
durations = _duration_count()
with patch.object(
Auth.AppInstallationAuth,
EXCHANGE,
side_effect=[_authorization(expired=True), _authorization(expired=False)],
) as exchange:
assert installation_auth.token == TEST_TOKEN # first exchange
assert installation_auth.token == TEST_TOKEN # expired, so refresh
assert installation_auth.token == TEST_TOKEN # fresh, no exchange
assert exchange.call_count == 2
assert _counter("success") == successes + 2
assert _duration_count() == durations + 2
> assert [c.args for c in span.call_args_list] == [
(
"github.get_installation_token",
{"github.installation_id": 777, "github.token_refresh": False},
),
(
"github.get_installation_token",
{"github.installation_id": 777, "github.token_refresh": True},
),
]
E AssertionError: assert [('github.get...resh': True})] == [('github.get...resh': True})]
E
E At index 0 diff: ('github.get_installation_token', {'github.installation_id': 777, 'github.token_refresh': True}) != ('github.get_installation_token', {'github.installation_id': 777, 'github.token_refresh': False})
E
E Full diff:
E [
E (
E 'github.get_installation_token',
E {
E 'github.installation_id': 777,
E - 'github.token_refresh': False,
E ? ^^^^
E + 'github.token_refresh': True,
E ? ^^^
E },
E ),
E (
E 'github.get_installation_token',
E {
E 'github.installation_id': 777,
E 'github.token_refresh': True,
E },
E ),
E ]
tests/test_installation_auth.py:74: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_token_exchanges_once_and_refreshes_when_expired - AssertionError: assert [('github.get...resh': True})] == [('github.get...resh': True})]
At index 0 diff: ('github.get_installation_token', {'github.installation_id': 777, 'github.token_refresh': True}) != ('github.get_installation_token', {'github.installation_id': 777, 'github.token_refresh': False})
Full diff:
[
(
'github.get_installation_token',
{
'github.installation_id': 777,
- 'github.token_refresh': False,
? ^^^^
+ 'github.token_refresh': True,
? ^^^
},
),
(
'github.get_installation_token',
{
'github.installation_id': 777,
'github.token_refresh': True,
},
),
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 182 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 0, definition_name: _get_installation_authorization
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -75,7 +75,7 @@
) as span:
try:
authorization = super()._get_installation_authorization()
- except Exception as e:
+ except CosmicRayTestingException as e:
github_api_requests_total.labels(operation="get_token", status="failure").inc()
set_span_error(span, e)
raise........................................................................ [ 15%]
........................................................................ [ 31%]
.......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_failed_exchange_is_counted_and_raised __
self = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7fae4b282ad0>
def _get_installation_authorization(self) -> InstallationAuthorization:
"""Exchange the App JWT for an installation token, with a span and metrics.
Returns:
The new installation token and its expiry.
Raises:
github.GithubException: If GitHub rejects the exchange.
"""
start_time = time.time()
with create_span(
"github.get_installation_token",
{
"github.installation_id": self.installation_id,
"github.token_refresh": self._exchanged,
},
) as span:
try:
> authorization = super()._get_installation_authorization()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/installation_auth.py:77:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='_get_installation_authorization' id='140386556434416'>
args = (), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 401 {"message": "Bad credentials"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7fae4b5a79d0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7fae4b282ad0>
span = <MagicMock name='create_span' id='140386556436432'>
def test_failed_exchange_is_counted_and_raised(self, installation_auth, span):
"""Test a rejected exchange increments the failure counter and propagates."""
failure = GithubException(401, {"message": "Bad credentials"}, None)
failures = _counter("failure")
durations = _duration_count()
with patch.object(Auth.AppInstallationAuth, EXCHANGE, side_effect=failure):
with pytest.raises(GithubException) as error:
> installation_auth.token # noqa: B018
^^^^^^^^^^^^^^^^^^^^^^^
tests/test_installation_auth.py:92:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/installation_auth.py:57: in token
return super().token
^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/github/Auth.py:376: in token
self.__installation_authorization = self._get_installation_authorization()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7fae4b282ad0>
def _get_installation_authorization(self) -> InstallationAuthorization:
"""Exchange the App JWT for an installation token, with a span and metrics.
Returns:
The new installation token and its expiry.
Raises:
github.GithubException: If GitHub rejects the exchange.
"""
start_time = time.time()
with create_span(
"github.get_installation_token",
{
"github.installation_id": self.installation_id,
"github.token_refresh": self._exchanged,
},
) as span:
try:
authorization = super()._get_installation_authorization()
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/installation_auth.py:78: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_failed_exchange_is_counted_and_raised - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 183 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 0, definition_name: InstrumentedInstallationAuth
--- mutation diff ---
--- a/stampbot/installation_auth.py
+++ b/stampbot/installation_auth.py
@@ -42,8 +42,6 @@
super().__init__(app_auth, installation_id)
self._exchange_lock = threading.Lock()
self._exchanged = False
-
- @property
def token(self) -> str:
"""Return the installation token, exchanging or refreshing it under a lock.
........................................................................ [ 15%]
........................................................................ [ 31%]
......................................F
=================================== FAILURES ===================================
_ TestInstrumentedInstallationAuth.test_token_exchanges_once_and_refreshes_when_expired _
self = <tests.test_installation_auth.TestInstrumentedInstallationAuth object at 0x7fc260920cd0>
installation_auth = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7fc25be7d7f0>
span = <MagicMock name='create_span' id='140472742311120'>
def test_token_exchanges_once_and_refreshes_when_expired(self, installation_auth, span):
"""Test PyGithub's ``token`` routes first exchange and refresh through the override."""
successes = _counter("success")
durations = _duration_count()
with patch.object(
Auth.AppInstallationAuth,
EXCHANGE,
side_effect=[_authorization(expired=True), _authorization(expired=False)],
) as exchange:
> assert installation_auth.token == TEST_TOKEN # first exchange
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError: assert token == 'test-token'
E + where token = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7fc25be7d7f0>.token
tests/test_installation_auth.py:67: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_installation_auth.py::TestInstrumentedInstallationAuth::test_token_exchanges_once_and_refreshes_when_expired - AssertionError: assert token == 'test-token'
+ where token = <stampbot.installation_auth.InstrumentedInstallationAuth object at 0x7fc25be7d7f0>.token
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 182 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Sub, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" - hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f8f0dba3680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f8f0e0e5010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" - hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: unsupported operand type(s) for -: 'str' and 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: unsupported operand type(s) for -: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Sub, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands - repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc864ced6d0>
mock_github_client = <MagicMock name='github_client' id='140498654556720'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fc864ced6d0>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands - repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for -: 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:57:56.782809Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:57:56.783437Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:57:56.782809Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:57:56.783437Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for -: 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Sub, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ - ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f5cfafc02f0>
mock_github_client = <MagicMock name='github_client' id='140037320102912'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f5cfafc02f0>
repo_config = <stampbot.config.RepoConfig object at 0x7f5cfb3e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
- ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: unsupported operand type(s) for -: 'str' and 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:03:54.174880Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:03:54.174880Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for -: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Sub, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ - ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f933e157d10>
mock_github_client = <MagicMock name='github_client' id='140270377276416'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f933e157d10>
repo_config = <stampbot.config.RepoConfig object at 0x7f933e8e0890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
- ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: unsupported operand type(s) for -: 'str' and 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:43:16.106844Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:43:16.106844Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for -: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Sub, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " - ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb513afd130>
mock_github_client = <MagicMock name='github_client' id='140415697949696'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fb513afd130>
repo_config = <stampbot.config.RepoConfig object at 0x7fb513edc890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " - ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: unsupported operand type(s) for -: 'str' and 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:39:04.850803Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:39:04.850803Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for -: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Sub, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " - ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f090b711130>
mock_github_client = <MagicMock name='github_client' id='139676820067328'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f090b711130>
repo_config = <stampbot.config.RepoConfig object at 0x7f090ba64890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " - ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: unsupported operand type(s) for -: 'str' and 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:53:49.999904Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:53:49.999904Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for -: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mul, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" * hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fac4327b680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fac437e9010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" * hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: can't multiply sequence by non-int of type 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: can't multiply sequence by non-int of type 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mul, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands * repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1b4ccf0b00>
mock_github_client = <MagicMock name='github_client' id='139755222282800'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f1b4ccf0b00>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands * repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: can't multiply sequence by non-int of type 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:18:45.109077Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:18:45.109792Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:18:45.109077Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:18:45.109792Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: can't multiply sequence by non-int of type 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mul, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ * ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f97145b7d10>
mock_github_client = <MagicMock name='github_client' id='140286854059008'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f97145b7d10>
repo_config = <stampbot.config.RepoConfig object at 0x7f97149e0890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
* ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: can't multiply sequence by non-int of type 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:42:46.604082Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:42:46.604082Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: can't multiply sequence by non-int of type 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.53s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mul, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ * ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2663e6d130>
mock_github_client = <MagicMock name='github_client' id='139802857591808'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f2663e6d130>
repo_config = <stampbot.config.RepoConfig object at 0x7f26683e0890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
* ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: can't multiply sequence by non-int of type 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:35:14.330253Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:35:14.330253Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: can't multiply sequence by non-int of type 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.53s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mul, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " * ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fd3f3d20650>
mock_github_client = <MagicMock name='github_client' id='140548305064960'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fd3f3d20650>
repo_config = <stampbot.config.RepoConfig object at 0x7fd3f85e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " * ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: can't multiply sequence by non-int of type 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:22:11.777018Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:22:11.777018Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: can't multiply sequence by non-int of type 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mul, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " * ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f4d48ef4470>
mock_github_client = <MagicMock name='github_client' id='139969909347328'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f4d48ef4470>
repo_config = <stampbot.config.RepoConfig object at 0x7f4d493e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " * ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: can't multiply sequence by non-int of type 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:06:29.364243Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:06:29.364243Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: can't multiply sequence by non-int of type 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.56s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Div, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" / hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fd67078b680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fd670cd9010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" / hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: unsupported operand type(s) for /: 'str' and 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: unsupported operand type(s) for /: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Div, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands / repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f507a3f0b00>
mock_github_client = <MagicMock name='github_client' id='139983620174384'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f507a3f0b00>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands / repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for /: 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:18:19.552993Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:18:19.553592Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:18:19.552993Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:18:19.553592Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for /: 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Div, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ / ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f7b5cd3ca70>
mock_github_client = <MagicMock name='github_client' id='140167813714944'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f7b5cd3ca70>
repo_config = <stampbot.config.RepoConfig object at 0x7f7b5d268890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
/ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: unsupported operand type(s) for /: 'str' and 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:41:47.250731Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:41:47.250731Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for /: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.51s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Div, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ / ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f926273b2f0>
mock_github_client = <MagicMock name='github_client' id='140266693170176'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f926273b2f0>
repo_config = <stampbot.config.RepoConfig object at 0x7f9262ee0890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
/ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: unsupported operand type(s) for /: 'str' and 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:50:12.839063Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:50:12.839063Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for /: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Div, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " / ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f617601b650>
mock_github_client = <MagicMock name='github_client' id='140056567321600'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f617601b650>
repo_config = <stampbot.config.RepoConfig object at 0x7f61767e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " / ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: unsupported operand type(s) for /: 'str' and 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:53:50.000001Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:53:50.000001Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for /: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.54s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Div, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " / ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f92ec38f650>
mock_github_client = <MagicMock name='github_client' id='140269003117568'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f92ec38f650>
repo_config = <stampbot.config.RepoConfig object at 0x7f92ec9e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " / ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: unsupported operand type(s) for /: 'str' and 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:07:25.625792Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:07:25.625792Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for /: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_FloorDiv, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" // hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f24d5daf680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f24d62d1010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" // hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: unsupported operand type(s) for //: 'str' and 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: unsupported operand type(s) for //: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_FloorDiv, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands // repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc219df0200>
mock_github_client = <MagicMock name='github_client' id='140471627428400'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fc219df0200>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands // repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for //: 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:29:36.944749Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:29:36.945349Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:29:36.944749Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:29:36.945349Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for //: 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_FloorDiv, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ // ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f19383ac230>
mock_github_client = <MagicMock name='github_client' id='139746221282304'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f19383ac230>
repo_config = <stampbot.config.RepoConfig object at 0x7f19387e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
// ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: unsupported operand type(s) for //: 'str' and 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:57:14.779294Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:57:14.779294Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for //: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_FloorDiv, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ // ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7ff4e2809130>
mock_github_client = <MagicMock name='github_client' id='140689745286144'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7ff4e2809130>
repo_config = <stampbot.config.RepoConfig object at 0x7ff4e2c28890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
// ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: unsupported operand type(s) for //: 'str' and 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:11:05.736727Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:11:05.736727Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for //: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.52s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_FloorDiv, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " // ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f59a9704590>
mock_github_client = <MagicMock name='github_client' id='140023072150528'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f59a9704590>
repo_config = <stampbot.config.RepoConfig object at 0x7f59a9fe4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " // ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: unsupported operand type(s) for //: 'str' and 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:45:58.895808Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:45:58.895808Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for //: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.52s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_FloorDiv, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " // ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f56b3a642f0>
mock_github_client = <MagicMock name='github_client' id='140010354856960'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f56b3a642f0>
repo_config = <stampbot.config.RepoConfig object at 0x7f56b3fe8890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " // ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: unsupported operand type(s) for //: 'str' and 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:33:55.827607Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:33:55.827607Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for //: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mod, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" % hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f04a0cbf680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f04a11d9010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" % hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: not all arguments converted during string formatting
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: not all arguments converted during string formatting
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mod, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands % repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f7508ceeb10>
mock_github_client = <MagicMock name='github_client' id='140140626424368'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f7508ceeb10>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands % repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for %: 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:34:55.526938Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:34:55.527531Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:34:55.526938Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:34:55.527531Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for %: 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mod, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ % ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f5d7ca01130>
mock_github_client = <MagicMock name='github_client' id='140039496029184'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f5d7ca01130>
repo_config = <stampbot.config.RepoConfig object at 0x7f5d7cd60890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
% ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: not all arguments converted during string formatting
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:34:37.408821Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:34:37.408821Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: not all arguments converted during string formatting
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.54s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mod, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ % ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f96ea20e390>
mock_github_client = <MagicMock name='github_client' id='140286149399552'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f96ea20e390>
repo_config = <stampbot.config.RepoConfig object at 0x7f96ea9dc890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
% ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: not all arguments converted during string formatting
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:37:41.636779Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:37:41.636779Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: not all arguments converted during string formatting
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mod, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " % ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbb2362d130>
mock_github_client = <MagicMock name='github_client' id='140441725752320'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fbb2362d130>
repo_config = <stampbot.config.RepoConfig object at 0x7fbb23974890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " % ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: not all arguments converted during string formatting
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:25:26.000207Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:25:26.000207Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: not all arguments converted during string formatting
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mod, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " % ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbd178822d0>
mock_github_client = <MagicMock name='github_client' id='140450118341632'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fbd178822d0>
repo_config = <stampbot.config.RepoConfig object at 0x7fbd17ee0890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " % ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: not all arguments converted during string formatting
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:59:46.734449Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:59:46.734449Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: not all arguments converted during string formatting
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Pow, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" ** hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f1f3c7a3680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f1f3ccd5010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" ** hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Pow, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands ** repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f7fb6ae8200>
mock_github_client = <MagicMock name='github_client' id='140186495480368'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f7fb6ae8200>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands ** repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ** or pow(): 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:24:58.365207Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:24:58.365790Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:24:58.365207Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:24:58.365790Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for ** or pow(): 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Pow, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ ** ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe77c7bd130>
mock_github_client = <MagicMock name='github_client' id='140632198534144'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fe77c7bd130>
repo_config = <stampbot.config.RepoConfig object at 0x7fe77cbe4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
** ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:53:38.249226Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:53:38.249226Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.54s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Pow, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ ** ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7ff8899c00b0>
mock_github_client = <MagicMock name='github_client' id='140705432982528'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7ff8899c00b0>
repo_config = <stampbot.config.RepoConfig object at 0x7ff889de4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
** ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:50:19.606448Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:50:19.606448Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.57s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Pow, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " ** ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f72e8ee58b0>
mock_github_client = <MagicMock name='github_client' id='140131510637568'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f72e8ee58b0>
repo_config = <stampbot.config.RepoConfig object at 0x7f72e96e0890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " ** ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:25:56.166120Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:25:56.166120Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Pow, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " ** ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f836519c2f0>
mock_github_client = <MagicMock name='github_client' id='140202309555200'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f836519c2f0>
repo_config = <stampbot.config.RepoConfig object at 0x7f836545c890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " ** ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:59:21.438365Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:59:21.438365Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_RShift, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" >> hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f5adaf8b680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f5adb4e5010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" >> hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: unsupported operand type(s) for >>: 'str' and 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: unsupported operand type(s) for >>: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_RShift, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands >> repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fcecc3f16d0>
mock_github_client = <MagicMock name='github_client' id='140526091661872'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fcecc3f16d0>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands >> repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:40:38.021745Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:40:38.022317Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:40:38.021745Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:40:38.022317Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for >>: 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_RShift, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ >> ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f512c94e390>
mock_github_client = <MagicMock name='github_client' id='139986615227392'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f512c94e390>
repo_config = <stampbot.config.RepoConfig object at 0x7f512cfe4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
>> ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: unsupported operand type(s) for >>: 'str' and 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:16:53.374442Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:16:53.374442Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for >>: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_RShift, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ >> ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f79c3bee930>
mock_github_client = <MagicMock name='github_client' id='140160945165312'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f79c3bee930>
repo_config = <stampbot.config.RepoConfig object at 0x7f79c3de0890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
>> ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: unsupported operand type(s) for >>: 'str' and 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:31:17.482653Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:31:17.482653Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for >>: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.57s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_RShift, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " >> ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f5a2a374110>
mock_github_client = <MagicMock name='github_client' id='140025232102400'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f5a2a374110>
repo_config = <stampbot.config.RepoConfig object at 0x7f5a2abcc890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " >> ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: unsupported operand type(s) for >>: 'str' and 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:02:30.621368Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:02:30.621368Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for >>: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_RShift, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " >> ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f41be9d1130>
mock_github_client = <MagicMock name='github_client' id='139920343127040'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f41be9d1130>
repo_config = <stampbot.config.RepoConfig object at 0x7f41bedd8890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " >> ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: unsupported operand type(s) for >>: 'str' and 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:41:58.035049Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:41:58.035049Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for >>: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_LShift, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" << hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fc97879f680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fc978cc9010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" << hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: unsupported operand type(s) for <<: 'str' and 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: unsupported operand type(s) for <<: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_LShift, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands << repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f35bcbf0b00>
mock_github_client = <MagicMock name='github_client' id='139868769399344'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f35bcbf0b00>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands << repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:24:40.081330Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:24:40.082037Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:24:40.081330Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:24:40.082037Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for <<: 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_LShift, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ << ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa5a8744ad0>
mock_github_client = <MagicMock name='github_client' id='140349471773696'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fa5a8744ad0>
repo_config = <stampbot.config.RepoConfig object at 0x7fa5a8ee4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
<< ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: unsupported operand type(s) for <<: 'str' and 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:54:13.345729Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:54:13.345729Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for <<: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_LShift, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ << ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fce911f9130>
mock_github_client = <MagicMock name='github_client' id='140525169595392'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fce911f9130>
repo_config = <stampbot.config.RepoConfig object at 0x7fce915e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
<< ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: unsupported operand type(s) for <<: 'str' and 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:45:32.941382Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:45:32.941382Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for <<: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_LShift, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " << ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6f99c35130>
mock_github_client = <MagicMock name='github_client' id='140117294011392'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f6f99c35130>
repo_config = <stampbot.config.RepoConfig object at 0x7f6f9a0dc890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " << ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: unsupported operand type(s) for <<: 'str' and 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:18:06.187194Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:18:06.187194Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for <<: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_LShift, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " << ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa84443c5f0>
mock_github_client = <MagicMock name='github_client' id='140360675644416'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fa84443c5f0>
repo_config = <stampbot.config.RepoConfig object at 0x7fa844bd4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " << ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: unsupported operand type(s) for <<: 'str' and 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:50:42.029591Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:50:42.029591Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for <<: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.72s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitOr, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" | hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f04d2d73680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f04d32e1010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" | hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: unsupported operand type(s) for |: 'str' and 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: unsupported operand type(s) for |: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitOr, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands | repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f9da5bf0200>
mock_github_client = <MagicMock name='github_client' id='140315061842480'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f9da5bf0200>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands | repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:39:21.028438Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:39:21.029047Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:39:21.028438Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:39:21.029047Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for |: 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitOr, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ | ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f375081c2f0>
mock_github_client = <MagicMock name='github_client' id='139875547812864'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f375081c2f0>
repo_config = <stampbot.config.RepoConfig object at 0x7f3750ddc890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
| ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: unsupported operand type(s) for |: 'str' and 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:31:30.916877Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:31:30.916877Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for |: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitOr, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ | ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa65c4609b0>
mock_github_client = <MagicMock name='github_client' id='140352488395776'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fa65c4609b0>
repo_config = <stampbot.config.RepoConfig object at 0x7fa65cbd4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
| ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: unsupported operand type(s) for |: 'str' and 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:26:02.823577Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:26:02.823577Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for |: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.53s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitOr, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " | ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fcf6ae276b0>
mock_github_client = <MagicMock name='github_client' id='140528827634688'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fcf6ae276b0>
repo_config = <stampbot.config.RepoConfig object at 0x7fcf6b5e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " | ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: unsupported operand type(s) for |: 'str' and 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:23:29.160607Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:23:29.160607Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for |: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitOr, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " | ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fd11edd7350>
mock_github_client = <MagicMock name='github_client' id='140536140567552'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fd11edd7350>
repo_config = <stampbot.config.RepoConfig object at 0x7fd11f268890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " | ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: unsupported operand type(s) for |: 'str' and 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:58:10.422144Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:58:10.422144Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for |: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitAnd, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" & hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7fdd9279f680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fdd92cdd010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" & hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: unsupported operand type(s) for &: 'str' and 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: unsupported operand type(s) for &: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitAnd, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands & repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f58a5cfac30>
mock_github_client = <MagicMock name='github_client' id='140018706723376'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f58a5cfac30>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands & repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:51:04.183182Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:51:04.183783Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:51:04.183182Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:51:04.183783Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for &: 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitAnd, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ & ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7faae39f1130>
mock_github_client = <MagicMock name='github_client' id='140371935482880'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7faae39f1130>
repo_config = <stampbot.config.RepoConfig object at 0x7faae3de4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
& ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: unsupported operand type(s) for &: 'str' and 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:38:58.167844Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:38:58.167844Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for &: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitAnd, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ & ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2f1e50d130>
mock_github_client = <MagicMock name='github_client' id='139840345445376'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f2f1e50d130>
repo_config = <stampbot.config.RepoConfig object at 0x7f2f1e9e0890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
& ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: unsupported operand type(s) for &: 'str' and 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:00:28.180171Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:00:28.180171Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for &: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitAnd, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " & ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f0135d97830>
mock_github_client = <MagicMock name='github_client' id='139643174212608'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f0135d97830>
repo_config = <stampbot.config.RepoConfig object at 0x7f013636c890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " & ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: unsupported operand type(s) for &: 'str' and 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:32:19.210212Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:32:19.210212Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for &: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitAnd, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " & ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f33705b5130>
mock_github_client = <MagicMock name='github_client' id='139858900767744'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f33705b5130>
repo_config = <stampbot.config.RepoConfig object at 0x7f33709e0890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " & ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: unsupported operand type(s) for &: 'str' and 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:36:28.124065Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:36:28.124065Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for &: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitXor, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -115,7 +115,7 @@
return False
expected_signature = (
- "sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
+ "sha256=" ^ hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f31c19a3680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f31c1ee5010>
payload = b'{"test": "data"}', signature = 'sha256=invalid'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
> "sha256=" ^ hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
E TypeError: unsupported operand type(s) for ^: 'str' and 'str'
stampbot/webhook_handler.py:118: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: unsupported operand type(s) for ^: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.30s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitXor, occurrence: 1, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if command in repo_config.approve_commands ^ repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f469bae9130>
mock_github_client = <MagicMock name='github_client' id='139941229310512'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f469bae9130>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":
success = await self._post_help(
installation_id,
repo_full_name,
pr_number,
repo_config,
)
chatops_commands_total.labels(
command="help",
status="success" if success else "failure",
).inc()
add_span_attributes(
span, {"chatops.result": "help_posted" if success else "help_failed"}
)
set_span_ok(span)
return {
"status": "success" if success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
> if command in repo_config.approve_commands ^ repo_config.unapprove_commands:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'list' and 'list'
stampbot/webhook_handler.py:730: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:42:10.514570Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:42:10.515191Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:42:10.514570Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:42:10.515191Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - TypeError: unsupported operand type(s) for ^: 'list' and 'list'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitXor, occurrence: 2, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -864,7 +864,7 @@
if repo_config.required_labels:
filters.append(
"required labels: "
- + ", ".join(f"`{label}`" for label in repo_config.required_labels)
+ ^ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2710611130>
mock_github_client = <MagicMock name='github_client' id='139805751776256'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f2710611130>
repo_config = <stampbot.config.RepoConfig object at 0x7f271096c890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
> "required labels: "
^ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
E TypeError: unsupported operand type(s) for ^: 'str' and 'str'
stampbot/webhook_handler.py:866: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:26:16.650954Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:26:16.650954Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for ^: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.52s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitXor, occurrence: 3, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -869,7 +869,7 @@
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
- + ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
+ ^ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f10e121cb30>
mock_github_client = <MagicMock name='github_client' id='139710472836096'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f10e121cb30>
repo_config = <stampbot.config.RepoConfig object at 0x7f10e19e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
> "required title patterns: "
^ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
E TypeError: unsupported operand type(s) for ^: 'str' and 'str'
stampbot/webhook_handler.py:871: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:56:19.813302Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:56:19.813302Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for ^: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitXor, occurrence: 4, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -873,7 +873,7 @@
)
if repo_config.allowed_users:
filters.append(
- "allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
+ "allowed users: " ^ ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f8e34255f70>
mock_github_client = <MagicMock name='github_client' id='140248735994880'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f8e34255f70>
repo_config = <stampbot.config.RepoConfig object at 0x7f8e349cc890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
> "allowed users: " ^ ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
E TypeError: unsupported operand type(s) for ^: 'str' and 'str'
stampbot/webhook_handler.py:876: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:22:27.967543Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:22:27.967543Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for ^: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitXor, occurrence: 5, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -877,7 +877,7 @@
)
if repo_config.allowed_teams:
filters.append(
- "allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
+ "allowed teams: " ^ ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
if filters:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f8d5bb15130>
mock_github_client = <MagicMock name='github_client' id='140245103727616'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1000:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:708: in _handle_pr_comment
success = await self._post_help(
stampbot/webhook_handler.py:912: in _post_help
self._format_help_message(repo_config),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f8d5bb15130>
repo_config = <stampbot.config.RepoConfig object at 0x7f8d602e4890>
def _format_help_message(self, repo_config: RepoConfig) -> str:
"""Format a help message for the effective repository configuration.
Args:
repo_config: Repository configuration
Returns:
Markdown help text.
"""
lines = ["## Stampbot Help", ""]
lines.extend(
[
"### ChatOps Commands",
"",
(
f"Approve and unapprove commands require "
f"**{repo_config.chatops_required_permission}** permission or higher."
),
"",
]
)
approve_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.approve_commands
)
unapprove_commands = ", ".join(
f"`@stampbot {command}`" for command in repo_config.unapprove_commands
)
lines.extend(
[
f"- **Approve**: {approve_commands}",
f"- **Unapprove**: {unapprove_commands}",
"- **Help**: `@stampbot help`",
"",
"### Label-Based Auto-Approval",
"",
]
)
if repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
if repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
if repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
if repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
if repo_config.allowed_teams:
filters.append(
> "allowed teams: " ^ ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
E TypeError: unsupported operand type(s) for ^: 'str' and 'str'
stampbot/webhook_handler.py:880: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:59:34.964409Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:59:34.964409Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - TypeError: unsupported operand type(s) for ^: 'str' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.70s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() + start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() + start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() + start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.25s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() * start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.15s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() * start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() * start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() / start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() / start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.15s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() / start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() // start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() // start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() // start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() % start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.26s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() % start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() % start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() ** start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f04639cb5c0>
mock_github_client = <MagicMock name='github_client' id='139656820733472'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f04639cb5c0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
comment = 'Auto-approved by Stampbot (label: autoapprove)'
trigger_type = 'label', skip_existing_check = False, head_sha = 'abc123def456'
async def _approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str,
trigger_type: str,
skip_existing_check: bool = False,
head_sha: str | None = None,
) -> bool:
"""Approve a PR and track metrics.
Checks for existing active approvals first to avoid duplicate reviews.
Two replicas handling the ``opened`` and ``labeled`` events of one new
pull request can both pass that check, so after a successful approval
the reviews are read again and every Stampbot approval of the same head
except the oldest is dismissed. Approving twice and dismissing one is
preferred to deferring, which could miss an approval when the other
event never arrives.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
comment: Approval comment
trigger_type: What triggered the approval (label, chatops)
skip_existing_check: Whether to skip duplicate active approval detection
head_sha: Current PR head SHA, used to distinguish stale approvals
Returns:
True if successful (or already approved)
"""
with create_span(
"webhook.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"approval.trigger_type": trigger_type,
},
) as span:
if not skip_existing_check:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Unknown state. A duplicate is recoverable; a missed
# approval is not, so approve and let the cleanup sort it out.
logger.warning(
"Could not read reviews for PR #%d in %s; approving without a check",
pr_number,
repo_full_name,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
reviews = []
existing_approvals = self._active_approvals_for_head(reviews, head_sha)
if existing_approvals:
logger.info(
"PR #%d in %s already has active approval for current head, skipping",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"existing_review_ids": existing_approvals,
},
)
add_span_attributes(
span,
{
"approval.result": "already_approved",
"approval.existing_reviews": len(existing_approvals),
},
)
set_span_ok(span)
return True
start_time = time.time()
success = await run_in_threadpool(
github_client.approve_pr,
installation_id,
repo_full_name,
pr_number,
comment,
)
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/webhook_handler.py:1004: OverflowError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:20:58.808373Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:20:58.809141Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:20:58.809441Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:20:58.808373Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:20:58.809141Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:20:58.809441Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() ** start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..........................................F
=================================== FAILURES ===================================
_______________________ test_pr_unlabeled_no_bot_reviews _______________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f40935f1fd0>
mock_github_client = <MagicMock name='github_client' id='139915326567968'>
@pytest.mark.asyncio
async def test_pr_unlabeled_no_bot_reviews(webhook_handler, mock_github_client):
"""Test removing label when no bot reviews exist."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [] # No reviews to dismiss
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:700: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:53.389039Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:49:53.389571Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:53.389707Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "(34, 'Numerical result out of range')"}, "event": "Error dismissing approvals: (34, 'Numerical result out of range')", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T22:49:53.389969Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:53.389039Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:49:53.389571Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:53.389707Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "(34, 'Numerical result out of range')"}, 'event': "Error dismissing approvals: (34, 'Numerical result out of range')", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T22:49:53.389969Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_no_bot_reviews - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 402 passed, 3 deselected, 1 warning in 4.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() ** start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................F
=================================== FAILURES ===================================
________________________ test_pr_unlabeled_autoapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7faaeae8ebd0>
mock_github_client = <MagicMock name='github_client' id='140372061760080'>
@pytest.mark.asyncio
async def test_pr_unlabeled_autoapprove(webhook_handler, mock_github_client):
"""Test removing autoapprove label dismisses approvals."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 222, "state": "APPROVED", "commit_id": "abc123def456"},
]
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:674: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:31:26.679935Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:31:26.680672Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:31:26.680813Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "(34, 'Numerical result out of range')"}, "event": "Error dismissing approvals: (34, 'Numerical result out of range')", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T20:31:26.681283Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:31:26.679935Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:31:26.680672Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:31:26.680813Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "(34, 'Numerical result out of range')"}, 'event': "Error dismissing approvals: (34, 'Numerical result out of range')", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T20:31:26.681283Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_autoapprove - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 400 passed, 3 deselected, 1 warning in 4.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.............................................................F
=================================== FAILURES ===================================
___ test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] ____
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f2f969c9bf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
> reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
stampbot/webhook_handler.py:1072:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/concurrency.py:34: in run_in_threadpool
return await anyio.to_thread.run_sync(func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/to_thread.py:63: in run_sync
return await get_async_backend().run_sync_in_worker_thread(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:2596: in run_sync_in_worker_thread
return await future
^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1029: in run
result = context.run(func, *args)
^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='139842365820608'>
args = (12345, 'octocat/hello-world', 42), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E RuntimeError: GitHub unavailable
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: RuntimeError
During handling of the above exception, another exception occurred:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2f969c9bf0>
mock_github_client = <MagicMock name='github_client' id='139842365828672'>
failure = RuntimeError('GitHub unavailable')
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", [RuntimeError("GitHub unavailable"), None])
async def test_pr_duplicate_check_failure_keeps_approval_and_is_counted(
webhook_handler, mock_github_client, failure
):
"""Test a failed duplicate scan keeps the approval and reaches the failure counter.
The client reports a listing failure as None; an unexpected exception is
handled the same way.
"""
from stampbot.metrics import pr_dismissals_total
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [[], failure]
failures = pr_dismissals_total.labels(trigger_type="duplicate", status="failure")
before = failures._value.get()
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:166:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
stampbot/webhook_handler.py:1019: in _approve_pr
await self._dismiss_approvals(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f2f969c9bf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")
if keep_oldest:
# Duplicate cleanup only touches approvals proven to be of
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
reviews, head_sha, proven_only=True
)[1:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)
add_span_attributes(span, {"dismissal.reviews_found": len(review_ids)})
if not review_ids:
logger.debug(
"No Stampbot approvals to dismiss on PR #%d",
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
pr_dismissal_duration_seconds.observe(time.time() - start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True
# Dismiss each review
success = True
for review_id in review_ids:
result = await run_in_threadpool(
github_client.dismiss_approval,
installation_id,
repo_full_name,
pr_number,
review_id,
message,
)
success = success and result
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"
pr_dismissals_total.labels(trigger_type=trigger_type, status=status).inc()
add_span_attributes(span, {"dismissal.result": status})
set_span_ok(span)
return success
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/webhook_handler.py:1129: OverflowError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:39.642893Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:20:39.643429Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:39.643708Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:39.642893Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:20:39.643429Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:39.643708Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 349 passed, 3 deselected, 1 warning in 4.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() >> start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f026792b5c0>
mock_github_client = <MagicMock name='github_client' id='139648299578912'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f026792b5c0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
comment = 'Auto-approved by Stampbot (label: autoapprove)'
trigger_type = 'label', skip_existing_check = False, head_sha = 'abc123def456'
async def _approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str,
trigger_type: str,
skip_existing_check: bool = False,
head_sha: str | None = None,
) -> bool:
"""Approve a PR and track metrics.
Checks for existing active approvals first to avoid duplicate reviews.
Two replicas handling the ``opened`` and ``labeled`` events of one new
pull request can both pass that check, so after a successful approval
the reviews are read again and every Stampbot approval of the same head
except the oldest is dismissed. Approving twice and dismissing one is
preferred to deferring, which could miss an approval when the other
event never arrives.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
comment: Approval comment
trigger_type: What triggered the approval (label, chatops)
skip_existing_check: Whether to skip duplicate active approval detection
head_sha: Current PR head SHA, used to distinguish stale approvals
Returns:
True if successful (or already approved)
"""
with create_span(
"webhook.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"approval.trigger_type": trigger_type,
},
) as span:
if not skip_existing_check:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Unknown state. A duplicate is recoverable; a missed
# approval is not, so approve and let the cleanup sort it out.
logger.warning(
"Could not read reviews for PR #%d in %s; approving without a check",
pr_number,
repo_full_name,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
reviews = []
existing_approvals = self._active_approvals_for_head(reviews, head_sha)
if existing_approvals:
logger.info(
"PR #%d in %s already has active approval for current head, skipping",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"existing_review_ids": existing_approvals,
},
)
add_span_attributes(
span,
{
"approval.result": "already_approved",
"approval.existing_reviews": len(existing_approvals),
},
)
set_span_ok(span)
return True
start_time = time.time()
success = await run_in_threadpool(
github_client.approve_pr,
installation_id,
repo_full_name,
pr_number,
comment,
)
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/webhook_handler.py:1004: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:22:17.988196Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:22:17.988977Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:22:17.989286Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:22:17.988196Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:22:17.988977Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:22:17.989286Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() >> start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..........................................F
=================================== FAILURES ===================================
_______________________ test_pr_unlabeled_no_bot_reviews _______________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f070065a510>
mock_github_client = <MagicMock name='github_client' id='139668046886432'>
@pytest.mark.asyncio
async def test_pr_unlabeled_no_bot_reviews(webhook_handler, mock_github_client):
"""Test removing label when no bot reviews exist."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [] # No reviews to dismiss
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:700: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:06:34.857195Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:06:34.857809Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:06:34.857962Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for >>: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for >>: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T21:06:34.858234Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:06:34.857195Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:06:34.857809Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:06:34.857962Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T21:06:34.858234Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_no_bot_reviews - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 402 passed, 3 deselected, 1 warning in 4.30s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() >> start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................F
=================================== FAILURES ===================================
________________________ test_pr_unlabeled_autoapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2765e5ca10>
mock_github_client = <MagicMock name='github_client' id='139807187200928'>
@pytest.mark.asyncio
async def test_pr_unlabeled_autoapprove(webhook_handler, mock_github_client):
"""Test removing autoapprove label dismisses approvals."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 222, "state": "APPROVED", "commit_id": "abc123def456"},
]
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:674: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:41:54.471795Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:41:54.472347Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:41:54.472501Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for >>: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for >>: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T20:41:54.472971Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:41:54.471795Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:41:54.472347Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:41:54.472501Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T20:41:54.472971Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_autoapprove - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 400 passed, 3 deselected, 1 warning in 4.29s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.............................................................F
=================================== FAILURES ===================================
___ test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] ____
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fcfecce5bf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
> reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
stampbot/webhook_handler.py:1072:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/concurrency.py:34: in run_in_threadpool
return await anyio.to_thread.run_sync(func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/to_thread.py:63: in run_sync
return await get_async_backend().run_sync_in_worker_thread(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:2596: in run_sync_in_worker_thread
return await future
^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1029: in run
result = context.run(func, *args)
^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='140531005574848'>
args = (12345, 'octocat/hello-world', 42), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E RuntimeError: GitHub unavailable
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: RuntimeError
During handling of the above exception, another exception occurred:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fcfecce5bf0>
mock_github_client = <MagicMock name='github_client' id='140531005582912'>
failure = RuntimeError('GitHub unavailable')
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", [RuntimeError("GitHub unavailable"), None])
async def test_pr_duplicate_check_failure_keeps_approval_and_is_counted(
webhook_handler, mock_github_client, failure
):
"""Test a failed duplicate scan keeps the approval and reaches the failure counter.
The client reports a listing failure as None; an unexpected exception is
handled the same way.
"""
from stampbot.metrics import pr_dismissals_total
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [[], failure]
failures = pr_dismissals_total.labels(trigger_type="duplicate", status="failure")
before = failures._value.get()
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:166:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
stampbot/webhook_handler.py:1019: in _approve_pr
await self._dismiss_approvals(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fcfecce5bf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")
if keep_oldest:
# Duplicate cleanup only touches approvals proven to be of
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
reviews, head_sha, proven_only=True
)[1:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)
add_span_attributes(span, {"dismissal.reviews_found": len(review_ids)})
if not review_ids:
logger.debug(
"No Stampbot approvals to dismiss on PR #%d",
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
pr_dismissal_duration_seconds.observe(time.time() - start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True
# Dismiss each review
success = True
for review_id in review_ids:
result = await run_in_threadpool(
github_client.dismiss_approval,
installation_id,
repo_full_name,
pr_number,
review_id,
message,
)
success = success and result
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"
pr_dismissals_total.labels(trigger_type=trigger_type, status=status).inc()
add_span_attributes(span, {"dismissal.result": status})
set_span_ok(span)
return success
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/webhook_handler.py:1129: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:55:22.975204Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:55:22.975780Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:55:22.976041Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:55:22.975204Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:55:22.975780Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:55:22.976041Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 349 passed, 3 deselected, 1 warning in 4.29s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() << start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2bf93ab5c0>
mock_github_client = <MagicMock name='github_client' id='139826840270368'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f2bf93ab5c0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
comment = 'Auto-approved by Stampbot (label: autoapprove)'
trigger_type = 'label', skip_existing_check = False, head_sha = 'abc123def456'
async def _approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str,
trigger_type: str,
skip_existing_check: bool = False,
head_sha: str | None = None,
) -> bool:
"""Approve a PR and track metrics.
Checks for existing active approvals first to avoid duplicate reviews.
Two replicas handling the ``opened`` and ``labeled`` events of one new
pull request can both pass that check, so after a successful approval
the reviews are read again and every Stampbot approval of the same head
except the oldest is dismissed. Approving twice and dismissing one is
preferred to deferring, which could miss an approval when the other
event never arrives.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
comment: Approval comment
trigger_type: What triggered the approval (label, chatops)
skip_existing_check: Whether to skip duplicate active approval detection
head_sha: Current PR head SHA, used to distinguish stale approvals
Returns:
True if successful (or already approved)
"""
with create_span(
"webhook.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"approval.trigger_type": trigger_type,
},
) as span:
if not skip_existing_check:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Unknown state. A duplicate is recoverable; a missed
# approval is not, so approve and let the cleanup sort it out.
logger.warning(
"Could not read reviews for PR #%d in %s; approving without a check",
pr_number,
repo_full_name,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
reviews = []
existing_approvals = self._active_approvals_for_head(reviews, head_sha)
if existing_approvals:
logger.info(
"PR #%d in %s already has active approval for current head, skipping",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"existing_review_ids": existing_approvals,
},
)
add_span_attributes(
span,
{
"approval.result": "already_approved",
"approval.existing_reviews": len(existing_approvals),
},
)
set_span_ok(span)
return True
start_time = time.time()
success = await run_in_threadpool(
github_client.approve_pr,
installation_id,
repo_full_name,
pr_number,
comment,
)
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/webhook_handler.py:1004: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:58:16.770841Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:58:16.771663Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:58:16.771965Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:58:16.770841Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:58:16.771663Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:58:16.771965Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() << start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..........................................F
=================================== FAILURES ===================================
_______________________ test_pr_unlabeled_no_bot_reviews _______________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1faf6ff7d0>
mock_github_client = <MagicMock name='github_client' id='139774063441440'>
@pytest.mark.asyncio
async def test_pr_unlabeled_no_bot_reviews(webhook_handler, mock_github_client):
"""Test removing label when no bot reviews exist."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [] # No reviews to dismiss
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:700: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:48:18.340251Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:48:18.340819Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:48:18.340959Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for <<: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for <<: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T21:48:18.341216Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:48:18.340251Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:48:18.340819Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:48:18.340959Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T21:48:18.341216Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_no_bot_reviews - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 402 passed, 3 deselected, 1 warning in 4.34s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() << start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................F
=================================== FAILURES ===================================
________________________ test_pr_unlabeled_autoapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f51207330b0>
mock_github_client = <MagicMock name='github_client' id='139986410875472'>
@pytest.mark.asyncio
async def test_pr_unlabeled_autoapprove(webhook_handler, mock_github_client):
"""Test removing autoapprove label dismisses approvals."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 222, "state": "APPROVED", "commit_id": "abc123def456"},
]
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:674: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:51:49.759313Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:51:49.759844Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:51:49.759984Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for <<: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for <<: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T20:51:49.760443Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:51:49.759313Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:51:49.759844Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:51:49.759984Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T20:51:49.760443Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_autoapprove - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 400 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.............................................................F
=================================== FAILURES ===================================
___ test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] ____
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f82641c6140>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
> reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
stampbot/webhook_handler.py:1072:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/concurrency.py:34: in run_in_threadpool
return await anyio.to_thread.run_sync(func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/to_thread.py:63: in run_sync
return await get_async_backend().run_sync_in_worker_thread(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:2596: in run_sync_in_worker_thread
return await future
^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1029: in run
result = context.run(func, *args)
^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='140197932715712'>
args = (12345, 'octocat/hello-world', 42), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E RuntimeError: GitHub unavailable
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: RuntimeError
During handling of the above exception, another exception occurred:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f82641c6140>
mock_github_client = <MagicMock name='github_client' id='140197932723776'>
failure = RuntimeError('GitHub unavailable')
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", [RuntimeError("GitHub unavailable"), None])
async def test_pr_duplicate_check_failure_keeps_approval_and_is_counted(
webhook_handler, mock_github_client, failure
):
"""Test a failed duplicate scan keeps the approval and reaches the failure counter.
The client reports a listing failure as None; an unexpected exception is
handled the same way.
"""
from stampbot.metrics import pr_dismissals_total
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [[], failure]
failures = pr_dismissals_total.labels(trigger_type="duplicate", status="failure")
before = failures._value.get()
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:166:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
stampbot/webhook_handler.py:1019: in _approve_pr
await self._dismiss_approvals(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f82641c6140>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")
if keep_oldest:
# Duplicate cleanup only touches approvals proven to be of
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
reviews, head_sha, proven_only=True
)[1:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)
add_span_attributes(span, {"dismissal.reviews_found": len(review_ids)})
if not review_ids:
logger.debug(
"No Stampbot approvals to dismiss on PR #%d",
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
pr_dismissal_duration_seconds.observe(time.time() - start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True
# Dismiss each review
success = True
for review_id in review_ids:
result = await run_in_threadpool(
github_client.dismiss_approval,
installation_id,
repo_full_name,
pr_number,
review_id,
message,
)
success = success and result
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"
pr_dismissals_total.labels(trigger_type=trigger_type, status=status).inc()
add_span_attributes(span, {"dismissal.result": status})
set_span_ok(span)
return success
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/webhook_handler.py:1129: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:31:36.951875Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:31:36.952407Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:31:36.952690Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:31:36.951875Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:31:36.952407Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:31:36.952690Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 349 passed, 3 deselected, 1 warning in 4.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() | start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fd195adf5c0>
mock_github_client = <MagicMock name='github_client' id='140538130495008'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fd195adf5c0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
comment = 'Auto-approved by Stampbot (label: autoapprove)'
trigger_type = 'label', skip_existing_check = False, head_sha = 'abc123def456'
async def _approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str,
trigger_type: str,
skip_existing_check: bool = False,
head_sha: str | None = None,
) -> bool:
"""Approve a PR and track metrics.
Checks for existing active approvals first to avoid duplicate reviews.
Two replicas handling the ``opened`` and ``labeled`` events of one new
pull request can both pass that check, so after a successful approval
the reviews are read again and every Stampbot approval of the same head
except the oldest is dismissed. Approving twice and dismissing one is
preferred to deferring, which could miss an approval when the other
event never arrives.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
comment: Approval comment
trigger_type: What triggered the approval (label, chatops)
skip_existing_check: Whether to skip duplicate active approval detection
head_sha: Current PR head SHA, used to distinguish stale approvals
Returns:
True if successful (or already approved)
"""
with create_span(
"webhook.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"approval.trigger_type": trigger_type,
},
) as span:
if not skip_existing_check:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Unknown state. A duplicate is recoverable; a missed
# approval is not, so approve and let the cleanup sort it out.
logger.warning(
"Could not read reviews for PR #%d in %s; approving without a check",
pr_number,
repo_full_name,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
reviews = []
existing_approvals = self._active_approvals_for_head(reviews, head_sha)
if existing_approvals:
logger.info(
"PR #%d in %s already has active approval for current head, skipping",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"existing_review_ids": existing_approvals,
},
)
add_span_attributes(
span,
{
"approval.result": "already_approved",
"approval.existing_reviews": len(existing_approvals),
},
)
set_span_ok(span)
return True
start_time = time.time()
success = await run_in_threadpool(
github_client.approve_pr,
installation_id,
repo_full_name,
pr_number,
comment,
)
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/webhook_handler.py:1004: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:03:16.827698Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:03:16.828573Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:03:16.828901Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:03:16.827698Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:03:16.828573Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:03:16.828901Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() | start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..........................................F
=================================== FAILURES ===================================
_______________________ test_pr_unlabeled_no_bot_reviews _______________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f5c2e4df9b0>
mock_github_client = <MagicMock name='github_client' id='140033889056288'>
@pytest.mark.asyncio
async def test_pr_unlabeled_no_bot_reviews(webhook_handler, mock_github_client):
"""Test removing label when no bot reviews exist."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [] # No reviews to dismiss
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:700: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:31:24.044912Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:31:24.045438Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:31:24.045592Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for |: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for |: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T21:31:24.045847Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:31:24.044912Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:31:24.045438Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:31:24.045592Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T21:31:24.045847Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_no_bot_reviews - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 402 passed, 3 deselected, 1 warning in 4.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() | start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................F
=================================== FAILURES ===================================
________________________ test_pr_unlabeled_autoapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f0769885850>
mock_github_client = <MagicMock name='github_client' id='139669811145632'>
@pytest.mark.asyncio
async def test_pr_unlabeled_autoapprove(webhook_handler, mock_github_client):
"""Test removing autoapprove label dismisses approvals."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 222, "state": "APPROVED", "commit_id": "abc123def456"},
]
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:674: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:16:21.815519Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:16:21.816049Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:16:21.816197Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for |: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for |: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T22:16:21.816683Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:16:21.815519Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:16:21.816049Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:16:21.816197Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T22:16:21.816683Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_autoapprove - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 400 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.............................................................F
=================================== FAILURES ===================================
___ test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] ____
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f3d84dce140>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
> reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
stampbot/webhook_handler.py:1072:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/concurrency.py:34: in run_in_threadpool
return await anyio.to_thread.run_sync(func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/to_thread.py:63: in run_sync
return await get_async_backend().run_sync_in_worker_thread(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:2596: in run_sync_in_worker_thread
return await future
^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1029: in run
result = context.run(func, *args)
^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='139902196534976'>
args = (12345, 'octocat/hello-world', 42), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E RuntimeError: GitHub unavailable
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: RuntimeError
During handling of the above exception, another exception occurred:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f3d84dce140>
mock_github_client = <MagicMock name='github_client' id='139902196543040'>
failure = RuntimeError('GitHub unavailable')
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", [RuntimeError("GitHub unavailable"), None])
async def test_pr_duplicate_check_failure_keeps_approval_and_is_counted(
webhook_handler, mock_github_client, failure
):
"""Test a failed duplicate scan keeps the approval and reaches the failure counter.
The client reports a listing failure as None; an unexpected exception is
handled the same way.
"""
from stampbot.metrics import pr_dismissals_total
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [[], failure]
failures = pr_dismissals_total.labels(trigger_type="duplicate", status="failure")
before = failures._value.get()
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:166:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
stampbot/webhook_handler.py:1019: in _approve_pr
await self._dismiss_approvals(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f3d84dce140>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")
if keep_oldest:
# Duplicate cleanup only touches approvals proven to be of
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
reviews, head_sha, proven_only=True
)[1:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)
add_span_attributes(span, {"dismissal.reviews_found": len(review_ids)})
if not review_ids:
logger.debug(
"No Stampbot approvals to dismiss on PR #%d",
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
pr_dismissal_duration_seconds.observe(time.time() - start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True
# Dismiss each review
success = True
for review_id in review_ids:
result = await run_in_threadpool(
github_client.dismiss_approval,
installation_id,
repo_full_name,
pr_number,
review_id,
message,
)
success = success and result
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"
pr_dismissals_total.labels(trigger_type=trigger_type, status=status).inc()
add_span_attributes(span, {"dismissal.result": status})
set_span_ok(span)
return success
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/webhook_handler.py:1129: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:41:58.319842Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:41:58.320385Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:41:58.320680Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:41:58.319842Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:41:58.320385Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:41:58.320680Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 349 passed, 3 deselected, 1 warning in 4.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() & start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe1c4da75c0>
mock_github_client = <MagicMock name='github_client' id='140607640073760'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fe1c4da75c0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
comment = 'Auto-approved by Stampbot (label: autoapprove)'
trigger_type = 'label', skip_existing_check = False, head_sha = 'abc123def456'
async def _approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str,
trigger_type: str,
skip_existing_check: bool = False,
head_sha: str | None = None,
) -> bool:
"""Approve a PR and track metrics.
Checks for existing active approvals first to avoid duplicate reviews.
Two replicas handling the ``opened`` and ``labeled`` events of one new
pull request can both pass that check, so after a successful approval
the reviews are read again and every Stampbot approval of the same head
except the oldest is dismissed. Approving twice and dismissing one is
preferred to deferring, which could miss an approval when the other
event never arrives.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
comment: Approval comment
trigger_type: What triggered the approval (label, chatops)
skip_existing_check: Whether to skip duplicate active approval detection
head_sha: Current PR head SHA, used to distinguish stale approvals
Returns:
True if successful (or already approved)
"""
with create_span(
"webhook.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"approval.trigger_type": trigger_type,
},
) as span:
if not skip_existing_check:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Unknown state. A duplicate is recoverable; a missed
# approval is not, so approve and let the cleanup sort it out.
logger.warning(
"Could not read reviews for PR #%d in %s; approving without a check",
pr_number,
repo_full_name,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
reviews = []
existing_approvals = self._active_approvals_for_head(reviews, head_sha)
if existing_approvals:
logger.info(
"PR #%d in %s already has active approval for current head, skipping",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"existing_review_ids": existing_approvals,
},
)
add_span_attributes(
span,
{
"approval.result": "already_approved",
"approval.existing_reviews": len(existing_approvals),
},
)
set_span_ok(span)
return True
start_time = time.time()
success = await run_in_threadpool(
github_client.approve_pr,
installation_id,
repo_full_name,
pr_number,
comment,
)
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/webhook_handler.py:1004: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:47:21.521593Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:47:21.522400Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:47:21.522721Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:47:21.521593Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:47:21.522400Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:47:21.522721Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() & start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..........................................F
=================================== FAILURES ===================================
_______________________ test_pr_unlabeled_no_bot_reviews _______________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f48e0d2d4f0>
mock_github_client = <MagicMock name='github_client' id='139950986311200'>
@pytest.mark.asyncio
async def test_pr_unlabeled_no_bot_reviews(webhook_handler, mock_github_client):
"""Test removing label when no bot reviews exist."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [] # No reviews to dismiss
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:700: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:13:01.034492Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:13:01.035029Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:13:01.035162Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for &: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for &: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T21:13:01.035416Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:13:01.034492Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:13:01.035029Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:13:01.035162Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T21:13:01.035416Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_no_bot_reviews - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 402 passed, 3 deselected, 1 warning in 4.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() & start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................F
=================================== FAILURES ===================================
________________________ test_pr_unlabeled_autoapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f00873671d0>
mock_github_client = <MagicMock name='github_client' id='139640242334624'>
@pytest.mark.asyncio
async def test_pr_unlabeled_autoapprove(webhook_handler, mock_github_client):
"""Test removing autoapprove label dismisses approvals."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 222, "state": "APPROVED", "commit_id": "abc123def456"},
]
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:674: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:04:56.320907Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:04:56.321494Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:04:56.321650Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for &: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for &: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T21:04:56.322148Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:04:56.320907Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:04:56.321494Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:04:56.321650Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T21:04:56.322148Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_autoapprove - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 400 passed, 3 deselected, 1 warning in 4.35s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.............................................................F
=================================== FAILURES ===================================
___ test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] ____
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f46a41b9bf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
> reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
stampbot/webhook_handler.py:1072:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/concurrency.py:34: in run_in_threadpool
return await anyio.to_thread.run_sync(func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/to_thread.py:63: in run_sync
return await get_async_backend().run_sync_in_worker_thread(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:2596: in run_sync_in_worker_thread
return await future
^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1029: in run
result = context.run(func, *args)
^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='139941307371200'>
args = (12345, 'octocat/hello-world', 42), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E RuntimeError: GitHub unavailable
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: RuntimeError
During handling of the above exception, another exception occurred:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f46a41b9bf0>
mock_github_client = <MagicMock name='github_client' id='139941307379264'>
failure = RuntimeError('GitHub unavailable')
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", [RuntimeError("GitHub unavailable"), None])
async def test_pr_duplicate_check_failure_keeps_approval_and_is_counted(
webhook_handler, mock_github_client, failure
):
"""Test a failed duplicate scan keeps the approval and reaches the failure counter.
The client reports a listing failure as None; an unexpected exception is
handled the same way.
"""
from stampbot.metrics import pr_dismissals_total
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [[], failure]
failures = pr_dismissals_total.labels(trigger_type="duplicate", status="failure")
before = failures._value.get()
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:166:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
stampbot/webhook_handler.py:1019: in _approve_pr
await self._dismiss_approvals(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f46a41b9bf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")
if keep_oldest:
# Duplicate cleanup only touches approvals proven to be of
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
reviews, head_sha, proven_only=True
)[1:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)
add_span_attributes(span, {"dismissal.reviews_found": len(review_ids)})
if not review_ids:
logger.debug(
"No Stampbot approvals to dismiss on PR #%d",
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
pr_dismissal_duration_seconds.observe(time.time() - start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True
# Dismiss each review
success = True
for review_id in review_ids:
result = await run_in_threadpool(
github_client.dismiss_approval,
installation_id,
repo_full_name,
pr_number,
review_id,
message,
)
success = success and result
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"
pr_dismissals_total.labels(trigger_type=trigger_type, status=status).inc()
add_span_attributes(span, {"dismissal.result": status})
set_span_ok(span)
return success
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/webhook_handler.py:1129: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:54:51.082280Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:54:51.082973Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:54:51.083246Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:54:51.082280Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:54:51.082973Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:54:51.083246Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 349 passed, 3 deselected, 1 warning in 4.30s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 0, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1001,7 +1001,7 @@
comment,
)
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
pr_approval_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f9af07fb5c0>
mock_github_client = <MagicMock name='github_client' id='140303429875232'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f9af07fb5c0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
comment = 'Auto-approved by Stampbot (label: autoapprove)'
trigger_type = 'label', skip_existing_check = False, head_sha = 'abc123def456'
async def _approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str,
trigger_type: str,
skip_existing_check: bool = False,
head_sha: str | None = None,
) -> bool:
"""Approve a PR and track metrics.
Checks for existing active approvals first to avoid duplicate reviews.
Two replicas handling the ``opened`` and ``labeled`` events of one new
pull request can both pass that check, so after a successful approval
the reviews are read again and every Stampbot approval of the same head
except the oldest is dismissed. Approving twice and dismissing one is
preferred to deferring, which could miss an approval when the other
event never arrives.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
comment: Approval comment
trigger_type: What triggered the approval (label, chatops)
skip_existing_check: Whether to skip duplicate active approval detection
head_sha: Current PR head SHA, used to distinguish stale approvals
Returns:
True if successful (or already approved)
"""
with create_span(
"webhook.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"approval.trigger_type": trigger_type,
},
) as span:
if not skip_existing_check:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Unknown state. A duplicate is recoverable; a missed
# approval is not, so approve and let the cleanup sort it out.
logger.warning(
"Could not read reviews for PR #%d in %s; approving without a check",
pr_number,
repo_full_name,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
reviews = []
existing_approvals = self._active_approvals_for_head(reviews, head_sha)
if existing_approvals:
logger.info(
"PR #%d in %s already has active approval for current head, skipping",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"existing_review_ids": existing_approvals,
},
)
add_span_attributes(
span,
{
"approval.result": "already_approved",
"approval.existing_reviews": len(existing_approvals),
},
)
set_span_ok(span)
return True
start_time = time.time()
success = await run_in_threadpool(
github_client.approve_pr,
installation_id,
repo_full_name,
pr_number,
comment,
)
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/webhook_handler.py:1004: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:37:05.744162Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:37:05.745000Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:37:05.745329Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:37:05.744162Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:37:05.745000Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:37:05.745329Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1096,7 +1096,7 @@
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
- pr_dismissal_duration_seconds.observe(time.time() - start_time)
+ pr_dismissal_duration_seconds.observe(time.time() ^ start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..........................................F
=================================== FAILURES ===================================
_______________________ test_pr_unlabeled_no_bot_reviews _______________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fee4b0df350>
mock_github_client = <MagicMock name='github_client' id='140661436462624'>
@pytest.mark.asyncio
async def test_pr_unlabeled_no_bot_reviews(webhook_handler, mock_github_client):
"""Test removing label when no bot reviews exist."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [] # No reviews to dismiss
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:700: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:53:17.134310Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:53:17.134853Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:53:17.134987Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for ^: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for ^: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T22:53:17.135239Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:53:17.134310Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:53:17.134853Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:53:17.134987Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T22:53:17.135239Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_no_bot_reviews - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 402 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1114,7 +1114,7 @@
)
success = success and result
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................F
=================================== FAILURES ===================================
________________________ test_pr_unlabeled_autoapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f7606c44dd0>
mock_github_client = <MagicMock name='github_client' id='140144895715920'>
@pytest.mark.asyncio
async def test_pr_unlabeled_autoapprove(webhook_handler, mock_github_client):
"""Test removing autoapprove label dismisses approvals."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 222, "state": "APPROVED", "commit_id": "abc123def456"},
]
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:674: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:53:31.651126Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:53:31.651676Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:53:31.651817Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "unsupported operand type(s) for ^: 'float' and 'float'"}, "event": "Error dismissing approvals: unsupported operand type(s) for ^: 'float' and 'float'", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T21:53:31.652276Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:53:31.651126Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:53:31.651676Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:53:31.651817Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Error dismissing approvals: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T21:53:31.652276Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_autoapprove - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 400 passed, 3 deselected, 1 warning in 4.26s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 3, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1126,7 +1126,7 @@
return success
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.............................................................F
=================================== FAILURES ===================================
___ test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] ____
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f25cfd15bf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
> reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
stampbot/webhook_handler.py:1072:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/concurrency.py:34: in run_in_threadpool
return await anyio.to_thread.run_sync(func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/to_thread.py:63: in run_sync
return await get_async_backend().run_sync_in_worker_thread(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:2596: in run_sync_in_worker_thread
return await future
^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1029: in run
result = context.run(func, *args)
^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='139800373989056'>
args = (12345, 'octocat/hello-world', 42), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E RuntimeError: GitHub unavailable
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: RuntimeError
During handling of the above exception, another exception occurred:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f25cfd15bf0>
mock_github_client = <MagicMock name='github_client' id='139800373997120'>
failure = RuntimeError('GitHub unavailable')
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", [RuntimeError("GitHub unavailable"), None])
async def test_pr_duplicate_check_failure_keeps_approval_and_is_counted(
webhook_handler, mock_github_client, failure
):
"""Test a failed duplicate scan keeps the approval and reaches the failure counter.
The client reports a listing failure as None; an unexpected exception is
handled the same way.
"""
from stampbot.metrics import pr_dismissals_total
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [[], failure]
failures = pr_dismissals_total.labels(trigger_type="duplicate", status="failure")
before = failures._value.get()
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:166:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
stampbot/webhook_handler.py:1019: in _approve_pr
await self._dismiss_approvals(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f25cfd15bf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")
if keep_oldest:
# Duplicate cleanup only touches approvals proven to be of
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
reviews, head_sha, proven_only=True
)[1:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)
add_span_attributes(span, {"dismissal.reviews_found": len(review_ids)})
if not review_ids:
logger.debug(
"No Stampbot approvals to dismiss on PR #%d",
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
pr_dismissal_duration_seconds.observe(time.time() - start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True
# Dismiss each review
success = True
for review_id in review_ids:
result = await run_in_threadpool(
github_client.dismiss_approval,
installation_id,
repo_full_name,
pr_number,
review_id,
message,
)
success = success and result
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"
pr_dismissals_total.labels(trigger_type=trigger_type, status=status).inc()
add_span_attributes(span, {"dismissal.result": status})
set_span_ok(span)
return success
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/webhook_handler.py:1129: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:01:57.930607Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:01:57.931151Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:01:57.931407Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:01:57.930607Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:01:57.931151Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:01:57.931407Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 349 passed, 3 deselected, 1 warning in 4.25s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Add, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ +,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E +,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Add, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ +,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E +,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Sub, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ -,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E -,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Sub, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ -,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E -,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.20s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Mul_Div, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ /,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first......................................................................... [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Mul_Div, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ /,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_FloorDiv, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ //,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E //,
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_FloorDiv, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ //,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E //,
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Mod, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ %,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E %,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Mod, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ %,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E %,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Pow, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ **,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E **,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Pow, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ **,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E **,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_RShift, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ >>,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E >>,
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_RShift, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ >>,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E >>,
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_LShift, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ <<,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E <<,
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_LShift, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ <<,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E <<,
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitOr, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ |,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E |,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitOr, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ |,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E |,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitAnd, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ &,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E &,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitAnd, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ &,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E &,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitXor, occurrence: 0, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -562,7 +562,7 @@
self,
reviews: list[BotReview],
head_sha: str | None,
- *,
+ ^,
proven_only: bool = False,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 565
E ^,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.30s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitXor, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1038,7 +1038,7 @@
pr_number: int,
message: str,
trigger_type: str,
- *,
+ ^,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
E File "/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py", line 1041
E ^,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 0, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -160,7 +160,7 @@
)
# Route to appropriate handler
- if event_type == "pull_request":
+ if event_type != "pull_request":
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7fe1ea1c99d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'error' == 'ok'
E
E - ok
E + error
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:09:32.826196Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in PR event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T22:09:32.826865Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:09:32.827739Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:09:32.826196Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:212 {'event': 'Missing required fields in PR event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T22:09:32.826865Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'error' == 'ok'
- ok
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 1, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -162,7 +162,7 @@
# Route to appropriate handler
if event_type == "pull_request":
result = await self._handle_pull_request(payload)
- elif event_type == "pull_request_review_comment":
+ elif event_type != "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
elif event_type == "issue_comment":
# Issue comments can be on PRs too........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7fd983e859d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:23:14.717756Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:23:14.719187Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:23:14.717756Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.56s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 2, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -164,7 +164,7 @@
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
- elif event_type == "issue_comment":
+ elif event_type != "issue_comment":
# Issue comments can be on PRs too
if "pull_request" in payload.get("issue", {}):
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f442b8959d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:24.892959Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:49:24.894344Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:24.892959Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.52s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 3, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -170,7 +170,7 @@
result = await self._handle_pr_comment(payload)
else:
result = {"status": "ignored", "message": "Not a PR comment"}
- elif event_type == "ping":
+ elif event_type != "ping":
result = {"status": "ok", "message": "pong"}
else:
result = {"status": "ignored", "message": f"Event type {event_type} not handled"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f739f3959d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:24:10.544975Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:24:10.546329Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:24:10.544975Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.54s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 4, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -244,7 +244,7 @@
},
)
- if action == "opened":
+ if action != "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
...F
=================================== FAILURES ===================================
_____ test_missing_repo_config_with_invalid_service_defaults_fails_closed ______
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f4f2213b290>
mock_github_client = <MagicMock name='github_client' id='139977845050064'>
@pytest.mark.asyncio
async def test_missing_repo_config_with_invalid_service_defaults_fails_closed(
webhook_handler, mock_github_client
):
"""Test invalid service defaults block a PR when no repo config exists."""
payload = load_fixture("pr_opened_with_autoapprove_label")
service_defaults = MagicMock(spec=["approve_commands"])
service_defaults.approve_commands = "approve"
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
result = await webhook_handler.handle_event("pull_request", payload)
assert result == {"status": "error", "message": "Invalid repository configuration"}
> review_body = mock_github_client.create_pr_review_comment.call_args.args[3]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'args'
tests/test_webhook_handler.py:1292: AttributeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:13:31.463767Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.repo_policy", "level": "warning", "timestamp": "2026-09-04T22:13:31.464319Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "warning"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T22:13:31.464443Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:13:31.463767Z'}
WARNING stampbot.repo_policy:repo_policy.py:142 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.repo_policy', 'level': 'warning', 'timestamp': '2026-09-04T22:13:31.464319Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T22:13:31.464443Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_missing_repo_config_with_invalid_service_defaults_fails_closed - AttributeError: 'NoneType' object has no attribute 'args'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 435 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 5, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -286,7 +286,7 @@
set_span_ok(span)
result = dict(NO_ACTION)
- if action == "opened":
+ if action != "opened":
# This only produces an operator warning, so it runs after the
# review decision instead of delaying it.
await self._warn_missing_approval_labels(event)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......F
=================================== FAILURES ===================================
__________________ test_pr_opened_missing_label_logs_warning ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fec2ce44c20>
mock_github_client = <MagicMock name='github_client' id='140652337761648'>
@pytest.mark.asyncio
async def test_pr_opened_missing_label_logs_warning(webhook_handler, mock_github_client):
"""Test missing approval label triggers label check after the review decision."""
payload = load_fixture("pr_opened_with_autoapprove_label")
mock_github_client.repo_has_label.return_value = False
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
# The label on the pull request exists by definition; only "stamp" is checked.
> mock_github_client.repo_has_label.assert_called_once_with(12345, "octocat/hello-world", "stamp")
tests/test_webhook_handler.py:352:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.repo_has_label' id='140652337767360'>
args = (12345, 'octocat/hello-world', 'stamp'), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:37:37.640597Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:37:37.641120Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:37:37.641369Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:37:37.640597Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:37:37.641120Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:37:37.641369Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_missing_label_logs_warning - AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 367 passed, 3 deselected, 1 warning in 4.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 6, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -306,7 +306,7 @@
if not config.auto_approve_on_label:
return Decision.NONE, None
- if event.action == "unlabeled":
+ if event.action != "unlabeled":
if event.event_label in config.approval_labels:
return Decision.DISMISS, event.event_label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb631ed35c0>
mock_github_client = <MagicMock name='github_client' id='140420492080672'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:32:54.648071Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:32:54.648834Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:32:54.648071Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:32:54.648834Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 3.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 7, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -322,7 +322,7 @@
# any duplicate afterwards.
return Decision.APPROVE, label
- if event.action == "labeled":
+ if event.action != "labeled":
if event.event_label in config.approval_labels:
return Decision.APPROVE, label
return Decision.REFRESH, label........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f51010e1940>
mock_github_client = <MagicMock name='github_client' id='139985881330224'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
> assert [r["status"] for r in results] == ["success", "success"]
E AssertionError: assert ['success', 'ignored'] == ['success', 'success']
E
E At index 1 diff: 'ignored' != 'success'
E
E Full diff:
E [
E 'success',
E - 'success',
E + 'ignored',
E ]
tests/test_webhook_handler.py:116: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:12:50.233886Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:12:50.234504Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:12:50.234781Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:12:50.235404Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:12:50.233886Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:12:50.234504Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:12:50.234781Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:12:50.235404Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: assert ['success', 'ignored'] == ['success', 'success']
At index 1 diff: 'ignored' != 'success'
Full diff:
[
'success',
- 'success',
+ 'ignored',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 8, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -327,7 +327,7 @@
return Decision.APPROVE, label
return Decision.REFRESH, label
- if event.action == "synchronize" and config.reapprove:
+ if event.action != "synchronize" and config.reapprove:
return Decision.REFRESH, label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f849bf5fa70>
mock_github_client = <MagicMock name='github_client' id='140207471638384'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:30:59.400382Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:30:59.400382Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 9, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -586,7 +586,7 @@
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
if proven_only:
- return bool(head_sha) and commit == head_sha
+ return bool(head_sha) and commit != head_sha
return not head_sha or not commit or commit == head_sha
return sorted(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f7b5cce08d0>
mock_github_client = <MagicMock name='github_client' id='140167813385248'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140167813385920'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:36:41.008810Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:36:41.009339Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:36:41.009613Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:36:41.008810Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:36:41.009339Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:36:41.009613Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 10, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha or not commit or commit != head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fd5c3ac9940>
mock_github_client = <MagicMock name='github_client' id='140556081426992'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140556081425312'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:19:19.844818Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:19:19.845391Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:19:19.845684Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:19:19.846323Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:19:19.846589Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:19:19.844818Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:19:19.845391Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:19:19.845684Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:19:19.846323Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:19:19.846589Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 11, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -592,7 +592,7 @@
return sorted(
review["id"]
for review in reviews
- if review["state"] == "APPROVED" and covers_head(review)
+ if review["state"] != "APPROVED" and covers_head(review)
)
def _has_current_active_approval(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6dabbd5940>
mock_github_client = <MagicMock name='github_client' id='140109002682928'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140109002681248'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:52:02.300048Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:52:02.300641Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:52:02.300917Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:52:02.301587Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:52:02.301847Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:52:02.300048Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:52:02.300641Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:52:02.300917Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:52:02.301587Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:52:02.301847Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 12, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -704,7 +704,7 @@
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
- if command == "help":
+ if command != "help":
success = await self._post_help(
installation_id,
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..................................................F
=================================== FAILURES ===================================
__________________________ test_issue_comment_approve __________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fab0e6f2b10>
mock_github_client = <MagicMock name='github_client' id='140372653855072'>
@pytest.mark.asyncio
async def test_issue_comment_approve(webhook_handler, mock_github_client):
"""Test @stampbot approve command."""
payload = load_fixture("issue_comment_approve")
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> assert "approved" in result["message"].lower()
E AssertionError: assert 'approved' in 'help message posted'
E + where 'help message posted' = <built-in method lower of str object at 0x7fab0eb171b0>()
E + where <built-in method lower of str object at 0x7fab0eb171b0> = 'Help message posted'.lower
tests/test_webhook_handler.py:830: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:04:29.488990Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:04:29.489564Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:04:29.488990Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:04:29.489564Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_approve - AssertionError: assert 'approved' in 'help message posted'
+ where 'help message posted' = <built-in method lower of str object at 0x7fab0eb171b0>()
+ where <built-in method lower of str object at 0x7fab0eb171b0> = 'Help message posted'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 410 passed, 3 deselected, 1 warning in 4.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 0, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -160,7 +160,7 @@
)
# Route to appropriate handler
- if event_type == "pull_request":
+ if event_type < "pull_request":
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f2678aad9d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'error' == 'ok'
E
E - ok
E + error
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:16:37.544340Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in PR event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:16:37.545004Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:16:37.545851Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:16:37.544340Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:212 {'event': 'Missing required fields in PR event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:16:37.545004Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'error' == 'ok'
- ok
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 1, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -162,7 +162,7 @@
# Route to appropriate handler
if event_type == "pull_request":
result = await self._handle_pull_request(payload)
- elif event_type == "pull_request_review_comment":
+ elif event_type < "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
elif event_type == "issue_comment":
# Issue comments can be on PRs too........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7fe3636499d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:02:54.895524Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:02:54.896866Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:02:54.895524Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 2, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -164,7 +164,7 @@
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
- elif event_type == "issue_comment":
+ elif event_type < "issue_comment":
# Issue comments can be on PRs too
if "pull_request" in payload.get("issue", {}):
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fd4bdced6d0>
mock_github_client = <MagicMock name='github_client' id='140551687303728'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:56:53.254159Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:56:53.254159Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 3, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -170,7 +170,7 @@
result = await self._handle_pr_comment(payload)
else:
result = {"status": "ignored", "message": "Not a PR comment"}
- elif event_type == "ping":
+ elif event_type < "ping":
result = {"status": "ok", "message": "pong"}
else:
result = {"status": "ignored", "message": f"Event type {event_type} not handled"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f107c3dd9d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:37:34.264520Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:37:34.265988Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:37:34.264520Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 4, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -244,7 +244,7 @@
},
)
- if action == "opened":
+ if action < "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
...F
=================================== FAILURES ===================================
_____ test_missing_repo_config_with_invalid_service_defaults_fails_closed ______
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fda03244290>
mock_github_client = <MagicMock name='github_client' id='140574331488976'>
@pytest.mark.asyncio
async def test_missing_repo_config_with_invalid_service_defaults_fails_closed(
webhook_handler, mock_github_client
):
"""Test invalid service defaults block a PR when no repo config exists."""
payload = load_fixture("pr_opened_with_autoapprove_label")
service_defaults = MagicMock(spec=["approve_commands"])
service_defaults.approve_commands = "approve"
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
result = await webhook_handler.handle_event("pull_request", payload)
assert result == {"status": "error", "message": "Invalid repository configuration"}
> review_body = mock_github_client.create_pr_review_comment.call_args.args[3]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'args'
tests/test_webhook_handler.py:1292: AttributeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:16:57.811967Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.repo_policy", "level": "warning", "timestamp": "2026-09-04T21:16:57.812554Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "warning"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:16:57.812682Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:16:57.811967Z'}
WARNING stampbot.repo_policy:repo_policy.py:142 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.repo_policy', 'level': 'warning', 'timestamp': '2026-09-04T21:16:57.812554Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:16:57.812682Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_missing_repo_config_with_invalid_service_defaults_fails_closed - AttributeError: 'NoneType' object has no attribute 'args'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 435 passed, 3 deselected, 1 warning in 4.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 5, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -286,7 +286,7 @@
set_span_ok(span)
result = dict(NO_ACTION)
- if action == "opened":
+ if action < "opened":
# This only produces an operator warning, so it runs after the
# review decision instead of delaying it.
await self._warn_missing_approval_labels(event)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......F
=================================== FAILURES ===================================
__________________ test_pr_opened_missing_label_logs_warning ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb30c721400>
mock_github_client = <MagicMock name='github_client' id='140406980742512'>
@pytest.mark.asyncio
async def test_pr_opened_missing_label_logs_warning(webhook_handler, mock_github_client):
"""Test missing approval label triggers label check after the review decision."""
payload = load_fixture("pr_opened_with_autoapprove_label")
mock_github_client.repo_has_label.return_value = False
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
# The label on the pull request exists by definition; only "stamp" is checked.
> mock_github_client.repo_has_label.assert_called_once_with(12345, "octocat/hello-world", "stamp")
tests/test_webhook_handler.py:352:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.repo_has_label' id='140406980746880'>
args = (12345, 'octocat/hello-world', 'stamp'), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:55:31.123763Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:55:31.124288Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:55:31.124564Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:55:31.123763Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:55:31.124288Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:55:31.124564Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_missing_label_logs_warning - AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 367 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 6, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -306,7 +306,7 @@
if not config.auto_approve_on_label:
return Decision.NONE, None
- if event.action == "unlabeled":
+ if event.action < "unlabeled":
if event.event_label in config.approval_labels:
return Decision.DISMISS, event.event_label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe818aef5c0>
mock_github_client = <MagicMock name='github_client' id='140634816575008'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:06:47.541081Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:06:47.541887Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:06:47.541081Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:06:47.541887Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 7, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -322,7 +322,7 @@
# any duplicate afterwards.
return Decision.APPROVE, label
- if event.action == "labeled":
+ if event.action < "labeled":
if event.event_label in config.approval_labels:
return Decision.APPROVE, label
return Decision.REFRESH, label........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f84aa5e1940>
mock_github_client = <MagicMock name='github_client' id='140207764206128'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
> assert [r["status"] for r in results] == ["success", "success"]
E AssertionError: assert ['success', 'ignored'] == ['success', 'success']
E
E At index 1 diff: 'ignored' != 'success'
E
E Full diff:
E [
E 'success',
E - 'success',
E + 'ignored',
E ]
tests/test_webhook_handler.py:116: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:45:50.932424Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:45:50.933068Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:45:50.933333Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:45:50.934008Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:45:50.932424Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:45:50.933068Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:45:50.933333Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:45:50.934008Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: assert ['success', 'ignored'] == ['success', 'success']
At index 1 diff: 'ignored' != 'success'
Full diff:
[
'success',
- 'success',
+ 'ignored',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 8, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -327,7 +327,7 @@
return Decision.APPROVE, label
return Decision.REFRESH, label
- if event.action == "synchronize" and config.reapprove:
+ if event.action < "synchronize" and config.reapprove:
return Decision.REFRESH, label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f89f7413a70>
mock_github_client = <MagicMock name='github_client' id='140230410172272'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:44:57.083883Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:44:57.083883Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 9, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -586,7 +586,7 @@
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
if proven_only:
- return bool(head_sha) and commit == head_sha
+ return bool(head_sha) and commit < head_sha
return not head_sha or not commit or commit == head_sha
return sorted(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f90fbe608d0>
mock_github_client = <MagicMock name='github_client' id='140260676242464'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140260676243136'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:31:23.601595Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:31:23.602161Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:31:23.602414Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:31:23.601595Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:31:23.602161Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:31:23.602414Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 10, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha or not commit or commit < head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f69811b5940>
mock_github_client = <MagicMock name='github_client' id='140091108028976'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140091108026960'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:34:09.393865Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:34:09.394497Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:34:09.394766Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:34:09.395420Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:34:09.395710Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:34:09.393865Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:34:09.394497Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:34:09.394766Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:34:09.395420Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:34:09.395710Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 11, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -592,7 +592,7 @@
return sorted(
review["id"]
for review in reviews
- if review["state"] == "APPROVED" and covers_head(review)
+ if review["state"] < "APPROVED" and covers_head(review)
)
def _has_current_active_approval(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f40b5575940>
mock_github_client = <MagicMock name='github_client' id='139915890897456'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='139915890896112'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:04.230811Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:29:04.231376Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:04.231676Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:04.232314Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:04.232596Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:04.230811Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:29:04.231376Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:04.231676Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:04.232314Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:04.232596Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 12, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -704,7 +704,7 @@
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
- if command == "help":
+ if command < "help":
success = await self._post_help(
installation_id,
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..................................................F
=================================== FAILURES ===================================
__________________________ test_issue_comment_approve __________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f294b3d1610>
mock_github_client = <MagicMock name='github_client' id='139815189696864'>
@pytest.mark.asyncio
async def test_issue_comment_approve(webhook_handler, mock_github_client):
"""Test @stampbot approve command."""
payload = load_fixture("issue_comment_approve")
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> assert "approved" in result["message"].lower()
E AssertionError: assert 'approved' in 'help message posted'
E + where 'help message posted' = <built-in method lower of str object at 0x7f294332f6b0>()
E + where <built-in method lower of str object at 0x7f294332f6b0> = 'Help message posted'.lower
tests/test_webhook_handler.py:830: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:01:54.298497Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:01:54.299065Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:01:54.298497Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:01:54.299065Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_approve - AssertionError: assert 'approved' in 'help message posted'
+ where 'help message posted' = <built-in method lower of str object at 0x7f294332f6b0>()
+ where <built-in method lower of str object at 0x7f294332f6b0> = 'Help message posted'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 410 passed, 3 deselected, 1 warning in 4.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 0, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -160,7 +160,7 @@
)
# Route to appropriate handler
- if event_type == "pull_request":
+ if event_type <= "pull_request":
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f26660d59d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'error' == 'ok'
E
E - ok
E + error
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:18:09.156962Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in PR event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T22:18:09.157615Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:18:09.158417Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:18:09.156962Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:212 {'event': 'Missing required fields in PR event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T22:18:09.157615Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'error' == 'ok'
- ok
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 1, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -162,7 +162,7 @@
# Route to appropriate handler
if event_type == "pull_request":
result = await self._handle_pull_request(payload)
- elif event_type == "pull_request_review_comment":
+ elif event_type <= "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
elif event_type == "issue_comment":
# Issue comments can be on PRs too........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f7ecffd59d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:08:06.548285Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:08:06.549697Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:08:06.548285Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.57s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 2, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -164,7 +164,7 @@
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
- elif event_type == "issue_comment":
+ elif event_type <= "issue_comment":
# Issue comments can be on PRs too
if "pull_request" in payload.get("issue", {}):
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.19s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 3, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -170,7 +170,7 @@
result = await self._handle_pr_comment(payload)
else:
result = {"status": "ignored", "message": "Not a PR comment"}
- elif event_type == "ping":
+ elif event_type <= "ping":
result = {"status": "ok", "message": "pong"}
else:
result = {"status": "ignored", "message": f"Event type {event_type} not handled"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 4, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -244,7 +244,7 @@
},
)
- if action == "opened":
+ if action <= "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
.......F
=================================== FAILURES ===================================
_______________ test_invalid_repo_config_no_review_on_non_opened _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fcadcddf110>
mock_github_client = <MagicMock name='github_client' id='140509261655280'>
@pytest.mark.asyncio
async def test_invalid_repo_config_no_review_on_non_opened(webhook_handler, mock_github_client):
"""Test invalid config skips review comment for non-opened events."""
payload = load_fixture("pr_opened_with_autoapprove_label")
payload["action"] = "labeled"
mock_github_client.get_repo_file.return_value = 'chatops_required_permission = "invalid"'
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "error"
> mock_github_client.create_pr_review_comment.assert_not_called()
tests/test_webhook_handler.py:1364:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.create_pr_review_comment' id='140509261658976'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'create_pr_review_comment' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Stampbot configuration error:\n\nInvalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin\n\nPlease fix the configuration to re-enable automation.')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:33:23.885687Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin"}, "event": "Invalid Stampbot configuration for octocat/hello-world: Invalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:33:23.886250Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:33:23.885687Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin'}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: Invalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:33:23.886250Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_invalid_repo_config_no_review_on_non_opened - AssertionError: Expected 'create_pr_review_comment' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Stampbot configuration error:\n\nInvalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin\n\nPlease fix the configuration to re-enable automation.')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 439 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 5, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -286,7 +286,7 @@
set_span_ok(span)
result = dict(NO_ACTION)
- if action == "opened":
+ if action <= "opened":
# This only produces an operator warning, so it runs after the
# review decision instead of delaying it.
await self._warn_missing_approval_labels(event)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 6, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -306,7 +306,7 @@
if not config.auto_approve_on_label:
return Decision.NONE, None
- if event.action == "unlabeled":
+ if event.action <= "unlabeled":
if event.event_label in config.approval_labels:
return Decision.DISMISS, event.event_label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbf0d95b5c0>
mock_github_client = <MagicMock name='github_client' id='140458543347232'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:44:17.889215Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:44:17.890077Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:44:17.889215Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:44:17.890077Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.18s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 7, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -322,7 +322,7 @@
# any duplicate afterwards.
return Decision.APPROVE, label
- if event.action == "labeled":
+ if event.action <= "labeled":
if event.event_label in config.approval_labels:
return Decision.APPROVE, label
return Decision.REFRESH, label........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.20s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 8, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -327,7 +327,7 @@
return Decision.APPROVE, label
return Decision.REFRESH, label
- if event.action == "synchronize" and config.reapprove:
+ if event.action <= "synchronize" and config.reapprove:
return Decision.REFRESH, label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 9, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -586,7 +586,7 @@
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
if proven_only:
- return bool(head_sha) and commit == head_sha
+ return bool(head_sha) and commit <= head_sha
return not head_sha or not commit or commit == head_sha
return sorted(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.F
=================================== FAILURES ===================================
_____ test_active_approvals_for_head_proven_only[reviews1-head-expected1] ______
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f689cac1130>
reviews = [{'id': 1, 'state': 'APPROVED', 'commit_id': None}], head_sha = 'head'
expected = []
@pytest.mark.parametrize(
("reviews", "head_sha", "expected"),
[
([{"id": 1, "state": "APPROVED", "commit_id": "head"}], None, []),
([{"id": 1, "state": "APPROVED", "commit_id": None}], "head", []),
(
[
{"id": 2, "state": "APPROVED", "commit_id": "head"},
{"id": 1, "state": "APPROVED", "commit_id": None},
{"id": 3, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2, 3],
),
],
)
def test_active_approvals_for_head_proven_only(webhook_handler, reviews, head_sha, expected):
"""Test duplicate cleanup never selects an approval on a guess."""
assert (
> webhook_handler._active_approvals_for_head(reviews, head_sha, proven_only=True) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
tests/test_webhook_handler.py:278:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:592: in _active_approvals_for_head
return sorted(
stampbot/webhook_handler.py:595: in <genexpr>
if review["state"] == "APPROVED" and covers_head(review)
^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
review = {'id': 1, 'state': 'APPROVED', 'commit_id': None}
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
if proven_only:
> return bool(head_sha) and commit <= head_sha
^^^^^^^^^^^^^^^^^^
E TypeError: '<=' not supported between instances of 'NoneType' and 'str'
stampbot/webhook_handler.py:589: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_active_approvals_for_head_proven_only[reviews1-head-expected1] - TypeError: '<=' not supported between instances of 'NoneType' and 'str'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 361 passed, 3 deselected, 1 warning in 4.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 10, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha or not commit or commit <= head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 11, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -592,7 +592,7 @@
return sorted(
review["id"]
for review in reviews
- if review["state"] == "APPROVED" and covers_head(review)
+ if review["state"] <= "APPROVED" and covers_head(review)
)
def _has_current_active_approval(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 12, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -704,7 +704,7 @@
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
- if command == "help":
+ if command <= "help":
success = await self._post_help(
installation_id,
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..................................................F
=================================== FAILURES ===================================
__________________________ test_issue_comment_approve __________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fed63ec3dd0>
mock_github_client = <MagicMock name='github_client' id='140657558524256'>
@pytest.mark.asyncio
async def test_issue_comment_approve(webhook_handler, mock_github_client):
"""Test @stampbot approve command."""
payload = load_fixture("issue_comment_approve")
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> assert "approved" in result["message"].lower()
E AssertionError: assert 'approved' in 'help message posted'
E + where 'help message posted' = <built-in method lower of str object at 0x7fed68607ef0>()
E + where <built-in method lower of str object at 0x7fed68607ef0> = 'Help message posted'.lower
tests/test_webhook_handler.py:830: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:30:56.220247Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:30:56.220810Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:30:56.220247Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:30:56.220810Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_approve - AssertionError: assert 'approved' in 'help message posted'
+ where 'help message posted' = <built-in method lower of str object at 0x7fed68607ef0>()
+ where <built-in method lower of str object at 0x7fed68607ef0> = 'Help message posted'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 410 passed, 3 deselected, 1 warning in 4.51s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 0, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -160,7 +160,7 @@
)
# Route to appropriate handler
- if event_type == "pull_request":
+ if event_type > "pull_request":
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................F
=================================== FAILURES ===================================
______________________________ test_unknown_event ______________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f13e1669090>
@pytest.mark.asyncio
async def test_unknown_event(webhook_handler):
"""Test handling unknown event type."""
payload = {}
result = await webhook_handler.handle_event("unknown", payload)
> assert result["status"] == "ignored"
E AssertionError: assert 'error' == 'ignored'
E
E - ignored
E + error
tests/test_webhook_handler.py:62: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "unknown", "action": ""}, "event": "Received unknown event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:14:39.540585Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in PR event", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:14:39.540727Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'unknown', 'action': ''}, 'event': 'Received unknown event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:14:39.540585Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:212 {'event': 'Missing required fields in PR event', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:14:39.540727Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_unknown_event - AssertionError: assert 'error' == 'ignored'
- ignored
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 344 passed, 3 deselected, 1 warning in 4.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 1, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -162,7 +162,7 @@
# Route to appropriate handler
if event_type == "pull_request":
result = await self._handle_pull_request(payload)
- elif event_type == "pull_request_review_comment":
+ elif event_type > "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
elif event_type == "issue_comment":
# Issue comments can be on PRs too........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
...............F
=================================== FAILURES ===================================
____________________ test_pull_request_review_comment_event ____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f933e560230>
mock_github_client = <MagicMock name='github_client' id='140270383350848'>
@pytest.mark.asyncio
async def test_pull_request_review_comment_event(webhook_handler, mock_github_client):
"""Test handling pull_request_review_comment event type."""
payload = {
"action": "created",
"pull_request": {
"number": 42,
},
"comment": {
"body": "@stampbot approve",
"user": {"login": "testuser"},
},
"repository": {
"full_name": "owner/repo",
},
"installation": {"id": 12345},
}
result = await webhook_handler.handle_event("pull_request_review_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:1487: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request_review_comment", "action": "created"}, "event": "Received pull_request_review_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:27:40.194503Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request_review_comment', 'action': 'created'}, 'event': 'Received pull_request_review_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:27:40.194503Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pull_request_review_comment_event - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 447 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 2, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -164,7 +164,7 @@
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
- elif event_type == "issue_comment":
+ elif event_type > "issue_comment":
# Issue comments can be on PRs too
if "pull_request" in payload.get("issue", {}):
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7fa97baad9d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:26:35.406289Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:26:35.407691Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:26:35.406289Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 3, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -170,7 +170,7 @@
result = await self._handle_pr_comment(payload)
else:
result = {"status": "ignored", "message": "Not a PR comment"}
- elif event_type == "ping":
+ elif event_type > "ping":
result = {"status": "ok", "message": "pong"}
else:
result = {"status": "ignored", "message": f"Event type {event_type} not handled"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f93937fd9d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:52:34.595169Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:52:34.596561Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:52:34.595169Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 4, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -244,7 +244,7 @@
},
)
- if action == "opened":
+ if action > "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
...F
=================================== FAILURES ===================================
_____ test_missing_repo_config_with_invalid_service_defaults_fails_closed ______
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc386383950>
mock_github_client = <MagicMock name='github_client' id='140477751831120'>
@pytest.mark.asyncio
async def test_missing_repo_config_with_invalid_service_defaults_fails_closed(
webhook_handler, mock_github_client
):
"""Test invalid service defaults block a PR when no repo config exists."""
payload = load_fixture("pr_opened_with_autoapprove_label")
service_defaults = MagicMock(spec=["approve_commands"])
service_defaults.approve_commands = "approve"
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
result = await webhook_handler.handle_event("pull_request", payload)
assert result == {"status": "error", "message": "Invalid repository configuration"}
> review_body = mock_github_client.create_pr_review_comment.call_args.args[3]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'args'
tests/test_webhook_handler.py:1292: AttributeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:56:53.220349Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.repo_policy", "level": "warning", "timestamp": "2026-09-04T20:56:53.220933Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "warning"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:56:53.221062Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:56:53.220349Z'}
WARNING stampbot.repo_policy:repo_policy.py:142 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.repo_policy', 'level': 'warning', 'timestamp': '2026-09-04T20:56:53.220933Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:56:53.221062Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_missing_repo_config_with_invalid_service_defaults_fails_closed - AttributeError: 'NoneType' object has no attribute 'args'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 435 passed, 3 deselected, 1 warning in 4.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 5, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -286,7 +286,7 @@
set_span_ok(span)
result = dict(NO_ACTION)
- if action == "opened":
+ if action > "opened":
# This only produces an operator warning, so it runs after the
# review decision instead of delaying it.
await self._warn_missing_approval_labels(event)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......F
=================================== FAILURES ===================================
__________________ test_pr_opened_missing_label_logs_warning ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc60fcb2510>
mock_github_client = <MagicMock name='github_client' id='140488639270256'>
@pytest.mark.asyncio
async def test_pr_opened_missing_label_logs_warning(webhook_handler, mock_github_client):
"""Test missing approval label triggers label check after the review decision."""
payload = load_fixture("pr_opened_with_autoapprove_label")
mock_github_client.repo_has_label.return_value = False
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
# The label on the pull request exists by definition; only "stamp" is checked.
> mock_github_client.repo_has_label.assert_called_once_with(12345, "octocat/hello-world", "stamp")
tests/test_webhook_handler.py:352:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.repo_has_label' id='140488639275632'>
args = (12345, 'octocat/hello-world', 'stamp'), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:55:59.238549Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:55:59.239094Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:55:59.239347Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:55:59.238549Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:55:59.239094Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:55:59.239347Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_missing_label_logs_warning - AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 367 passed, 3 deselected, 1 warning in 4.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 6, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -306,7 +306,7 @@
if not config.auto_approve_on_label:
return Decision.NONE, None
- if event.action == "unlabeled":
+ if event.action > "unlabeled":
if event.event_label in config.approval_labels:
return Decision.DISMISS, event.event_label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f54dfb82990>
mock_github_client = <MagicMock name='github_client' id='140002418472816'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "error"
E AssertionError: assert 'ignored' == 'error'
E
E - error
E + ignored
tests/test_webhook_handler.py:210: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:47:59.587939Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:47:59.588520Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:47:59.587939Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:47:59.588520Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - AssertionError: assert 'ignored' == 'error'
- error
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 7, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -322,7 +322,7 @@
# any duplicate afterwards.
return Decision.APPROVE, label
- if event.action == "labeled":
+ if event.action > "labeled":
if event.event_label in config.approval_labels:
return Decision.APPROVE, label
return Decision.REFRESH, label........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f8da7795940>
mock_github_client = <MagicMock name='github_client' id='140246370660912'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
> assert [r["status"] for r in results] == ["success", "success"]
E AssertionError: assert ['success', 'ignored'] == ['success', 'success']
E
E At index 1 diff: 'ignored' != 'success'
E
E Full diff:
E [
E 'success',
E - 'success',
E + 'ignored',
E ]
tests/test_webhook_handler.py:116: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:28:39.237371Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:28:39.238008Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:28:39.238275Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:28:39.238935Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:28:39.237371Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:28:39.238008Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:28:39.238275Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:28:39.238935Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: assert ['success', 'ignored'] == ['success', 'success']
At index 1 diff: 'ignored' != 'success'
Full diff:
[
'success',
- 'success',
+ 'ignored',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 8, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -327,7 +327,7 @@
return Decision.APPROVE, label
return Decision.REFRESH, label
- if event.action == "synchronize" and config.reapprove:
+ if event.action > "synchronize" and config.reapprove:
return Decision.REFRESH, label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f714ea63a70>
mock_github_client = <MagicMock name='github_client' id='140124570138480'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:42:32.118935Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:42:32.118935Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 9, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -586,7 +586,7 @@
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
if proven_only:
- return bool(head_sha) and commit == head_sha
+ return bool(head_sha) and commit > head_sha
return not head_sha or not commit or commit == head_sha
return sorted(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb6f48e48d0>
mock_github_client = <MagicMock name='github_client' id='140423762496544'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140423762497216'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:58:40.751443Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:58:40.752013Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:58:40.752262Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:58:40.751443Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:58:40.752013Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:58:40.752262Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 10, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha or not commit or commit > head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fef7aed1940>
mock_github_client = <MagicMock name='github_client' id='140666530624048'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140666530622032'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:00:10.205236Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:00:10.205838Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:00:10.206102Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:00:10.206773Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:00:10.207027Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:00:10.205236Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:00:10.205838Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:00:10.206102Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:00:10.206773Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:00:10.207027Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 11, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -592,7 +592,7 @@
return sorted(
review["id"]
for review in reviews
- if review["state"] == "APPROVED" and covers_head(review)
+ if review["state"] > "APPROVED" and covers_head(review)
)
def _has_current_active_approval(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f84730e5940>
mock_github_client = <MagicMock name='github_client' id='140206837396016'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140206837394000'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:42:50.727374Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:42:50.728022Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:42:50.728293Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:42:50.728956Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:42:50.729213Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:42:50.727374Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:42:50.728022Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:42:50.728293Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:42:50.728956Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:42:50.729213Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.26s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 12, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -704,7 +704,7 @@
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
- if command == "help":
+ if command > "help":
success = await self._post_help(
installation_id,
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......................................................F
=================================== FAILURES ===================================
_________________________ test_issue_comment_unapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f892c1aa750>
mock_github_client = <MagicMock name='github_client' id='140227127332208'>
@pytest.mark.asyncio
async def test_issue_comment_unapprove(webhook_handler, mock_github_client):
"""Test @stampbot unapprove command."""
payload = load_fixture("issue_comment_unapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"}
]
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> assert "dismissed" in result["message"].lower()
E AssertionError: assert 'dismissed' in 'help message posted'
E + where 'help message posted' = <built-in method lower of str object at 0x7f892cb1fa30>()
E + where <built-in method lower of str object at 0x7f892cb1fa30> = 'Help message posted'.lower
tests/test_webhook_handler.py:922: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:19:50.265882Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:19:50.266412Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:19:50.265882Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:19:50.266412Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_unapprove - AssertionError: assert 'dismissed' in 'help message posted'
+ where 'help message posted' = <built-in method lower of str object at 0x7f892cb1fa30>()
+ where <built-in method lower of str object at 0x7f892cb1fa30> = 'Help message posted'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 415 passed, 3 deselected, 1 warning in 4.57s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 0, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -160,7 +160,7 @@
)
# Route to appropriate handler
- if event_type == "pull_request":
+ if event_type >= "pull_request":
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................F
=================================== FAILURES ===================================
______________________________ test_unknown_event ______________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fd3d5317390>
@pytest.mark.asyncio
async def test_unknown_event(webhook_handler):
"""Test handling unknown event type."""
payload = {}
result = await webhook_handler.handle_event("unknown", payload)
> assert result["status"] == "ignored"
E AssertionError: assert 'error' == 'ignored'
E
E - ignored
E + error
tests/test_webhook_handler.py:62: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "unknown", "action": ""}, "event": "Received unknown event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:06:13.065999Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in PR event", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:06:13.066145Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'unknown', 'action': ''}, 'event': 'Received unknown event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:06:13.065999Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:212 {'event': 'Missing required fields in PR event', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:06:13.066145Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_unknown_event - AssertionError: assert 'error' == 'ignored'
- ignored
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 344 passed, 3 deselected, 1 warning in 4.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 1, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -162,7 +162,7 @@
# Route to appropriate handler
if event_type == "pull_request":
result = await self._handle_pull_request(payload)
- elif event_type == "pull_request_review_comment":
+ elif event_type >= "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
elif event_type == "issue_comment":
# Issue comments can be on PRs too........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 2, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -164,7 +164,7 @@
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
- elif event_type == "issue_comment":
+ elif event_type >= "issue_comment":
# Issue comments can be on PRs too
if "pull_request" in payload.get("issue", {}):
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7fc3711c19d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:54:06.454248Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:54:06.455753Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:54:06.454248Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 3, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -170,7 +170,7 @@
result = await self._handle_pr_comment(payload)
else:
result = {"status": "ignored", "message": "Not a PR comment"}
- elif event_type == "ping":
+ elif event_type >= "ping":
result = {"status": "ok", "message": "pong"}
else:
result = {"status": "ignored", "message": f"Event type {event_type} not handled"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................F
=================================== FAILURES ===================================
______________________________ test_unknown_event ______________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fdcfafd3390>
@pytest.mark.asyncio
async def test_unknown_event(webhook_handler):
"""Test handling unknown event type."""
payload = {}
result = await webhook_handler.handle_event("unknown", payload)
> assert result["status"] == "ignored"
E AssertionError: assert 'ok' == 'ignored'
E
E - ignored
E + ok
tests/test_webhook_handler.py:62: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "unknown", "action": ""}, "event": "Received unknown event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:08:23.455171Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'unknown', 'action': ''}, 'event': 'Received unknown event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:08:23.455171Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_unknown_event - AssertionError: assert 'ok' == 'ignored'
- ignored
+ ok
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 344 passed, 3 deselected, 1 warning in 4.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 4, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -244,7 +244,7 @@
},
)
- if action == "opened":
+ if action >= "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 5, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -286,7 +286,7 @@
set_span_ok(span)
result = dict(NO_ACTION)
- if action == "opened":
+ if action >= "opened":
# This only produces an operator warning, so it runs after the
# review decision instead of delaying it.
await self._warn_missing_approval_labels(event)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 6, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -306,7 +306,7 @@
if not config.auto_approve_on_label:
return Decision.NONE, None
- if event.action == "unlabeled":
+ if event.action >= "unlabeled":
if event.event_label in config.approval_labels:
return Decision.DISMISS, event.event_label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 7, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -322,7 +322,7 @@
# any duplicate afterwards.
return Decision.APPROVE, label
- if event.action == "labeled":
+ if event.action >= "labeled":
if event.event_label in config.approval_labels:
return Decision.APPROVE, label
return Decision.REFRESH, label........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
......................F
=================================== FAILURES ===================================
____________________ test_pr_synchronize_reapprove_disabled ____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f47eb29f530>
mock_github_client = <MagicMock name='github_client' id='139946864590336'>
@pytest.mark.asyncio
async def test_pr_synchronize_reapprove_disabled(webhook_handler, mock_github_client):
"""Test new commits do not re-approve by default."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "ignored"
assert "no action" in result["message"].lower()
> mock_github_client.find_bot_approval_reviews.assert_not_called()
tests/test_webhook_handler.py:531:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='139946865583040'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'find_bot_approval_reviews' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:57:44.882148Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:57:44.882740Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:57:44.882148Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:57:44.882740Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_reapprove_disabled - AssertionError: Expected 'find_bot_approval_reviews' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 382 passed, 3 deselected, 1 warning in 4.24s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 8, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -327,7 +327,7 @@
return Decision.APPROVE, label
return Decision.REFRESH, label
- if event.action == "synchronize" and config.reapprove:
+ if event.action >= "synchronize" and config.reapprove:
return Decision.REFRESH, label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 9, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -586,7 +586,7 @@
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
if proven_only:
- return bool(head_sha) and commit == head_sha
+ return bool(head_sha) and commit >= head_sha
return not head_sha or not commit or commit == head_sha
return sorted(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1dce0588d0>
mock_github_client = <MagicMock name='github_client' id='139765985348640'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='139765985349312'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
E call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:58:51.306943Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:58:51.307500Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:58:51.307761Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:58:51.306943Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:58:51.307500Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:58:51.307761Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 10, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha or not commit or commit >= head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.....................................................................F
=================================== FAILURES ===================================
___________ test_active_approvals_for_head[reviews3-head-expected3] ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f0ba32e16d0>
reviews = [{'id': 1, 'state': 'APPROVED', 'commit_id': 'old'}, {'id': 2, 'state': 'APPROVED', 'commit_id': 'head'}]
head_sha = 'head', expected = [2]
@pytest.mark.parametrize(
("reviews", "head_sha", "expected"),
[
([], "head", []),
([{"id": 1, "state": "APPROVED", "commit_id": "head"}], "head", [1]),
(
[
{"id": 3, "state": "APPROVED", "commit_id": "head"},
{"id": 1, "state": "APPROVED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[1, 2, 3],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": "old"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": None},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
None,
[1, 2],
),
(
[
{"id": 1, "state": "DISMISSED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
],
)
def test_active_approvals_for_head(webhook_handler, reviews, head_sha, expected):
"""Test which approvals count as covering the head, oldest first."""
> assert webhook_handler._active_approvals_for_head(reviews, head_sha) == expected
E assert [1, 2] == [2]
E
E At index 0 diff: 1 != 2
E Left contains one more item: 2
E
E Full diff:
E [
E + 1,
E 2,
E ]
tests/test_webhook_handler.py:256: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_active_approvals_for_head[reviews3-head-expected3] - assert [1, 2] == [2]
At index 0 diff: 1 != 2
Left contains one more item: 2
Full diff:
[
+ 1,
2,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 357 passed, 3 deselected, 1 warning in 4.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 11, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -592,7 +592,7 @@
return sorted(
review["id"]
for review in reviews
- if review["state"] == "APPROVED" and covers_head(review)
+ if review["state"] >= "APPROVED" and covers_head(review)
)
def _has_current_active_approval(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe8f46a08d0>
mock_github_client = <MagicMock name='github_client' id='140638507764768'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140638507765440'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
E call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:13:24.119422Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:13:24.119972Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:13:24.120219Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:13:24.119422Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:13:24.119972Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:13:24.120219Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 12, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -704,7 +704,7 @@
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
- if command == "help":
+ if command >= "help":
success = await self._post_help(
installation_id,
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......................................................F
=================================== FAILURES ===================================
_________________________ test_issue_comment_unapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fdfdc903050>
mock_github_client = <MagicMock name='github_client' id='140599454485872'>
@pytest.mark.asyncio
async def test_issue_comment_unapprove(webhook_handler, mock_github_client):
"""Test @stampbot unapprove command."""
payload = load_fixture("issue_comment_unapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"}
]
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> assert "dismissed" in result["message"].lower()
E AssertionError: assert 'dismissed' in 'help message posted'
E + where 'help message posted' = <built-in method lower of str object at 0x7fdfdd238130>()
E + where <built-in method lower of str object at 0x7fdfdd238130> = 'Help message posted'.lower
tests/test_webhook_handler.py:922: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:17:16.914089Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:17:16.914632Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:17:16.914089Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:17:16.914632Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_unapprove - AssertionError: assert 'dismissed' in 'help message posted'
+ where 'help message posted' = <built-in method lower of str object at 0x7fdfdd238130>()
+ where <built-in method lower of str object at 0x7fdfdd238130> = 'Help message posted'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 415 passed, 3 deselected, 1 warning in 4.50s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 0, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -160,7 +160,7 @@
)
# Route to appropriate handler
- if event_type == "pull_request":
+ if event_type is "pull_request":
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:163
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:163: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if event_type is "pull_request":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 1, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -162,7 +162,7 @@
# Route to appropriate handler
if event_type == "pull_request":
result = await self._handle_pull_request(payload)
- elif event_type == "pull_request_review_comment":
+ elif event_type is "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
elif event_type == "issue_comment":
# Issue comments can be on PRs too........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:165
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:165: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
elif event_type is "pull_request_review_comment":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 2, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -164,7 +164,7 @@
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
- elif event_type == "issue_comment":
+ elif event_type is "issue_comment":
# Issue comments can be on PRs too
if "pull_request" in payload.get("issue", {}):
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:167
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:167: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
elif event_type is "issue_comment":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 3, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -170,7 +170,7 @@
result = await self._handle_pr_comment(payload)
else:
result = {"status": "ignored", "message": "Not a PR comment"}
- elif event_type == "ping":
+ elif event_type is "ping":
result = {"status": "ok", "message": "pong"}
else:
result = {"status": "ignored", "message": f"Event type {event_type} not handled"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7fb3b30619d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:50:40.016651Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:50:40.018081Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:50:40.016651Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:173
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:173: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
elif event_type is "ping":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 2 warnings in 3.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 4, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -244,7 +244,7 @@
},
)
- if action == "opened":
+ if action is "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
...F
=================================== FAILURES ===================================
_____ test_missing_repo_config_with_invalid_service_defaults_fails_closed ______
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6b3a283c50>
mock_github_client = <MagicMock name='github_client' id='140098510408400'>
@pytest.mark.asyncio
async def test_missing_repo_config_with_invalid_service_defaults_fails_closed(
webhook_handler, mock_github_client
):
"""Test invalid service defaults block a PR when no repo config exists."""
payload = load_fixture("pr_opened_with_autoapprove_label")
service_defaults = MagicMock(spec=["approve_commands"])
service_defaults.approve_commands = "approve"
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
result = await webhook_handler.handle_event("pull_request", payload)
assert result == {"status": "error", "message": "Invalid repository configuration"}
> review_body = mock_github_client.create_pr_review_comment.call_args.args[3]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'args'
tests/test_webhook_handler.py:1292: AttributeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:29:44.015098Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.repo_policy", "level": "warning", "timestamp": "2026-09-04T22:29:44.016045Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "warning"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T22:29:44.016198Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:29:44.015098Z'}
WARNING stampbot.repo_policy:repo_policy.py:142 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.repo_policy', 'level': 'warning', 'timestamp': '2026-09-04T22:29:44.016045Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T22:29:44.016198Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:247
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:247: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if action is "opened":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_missing_repo_config_with_invalid_service_defaults_fails_closed - AttributeError: 'NoneType' object has no attribute 'args'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 435 passed, 3 deselected, 2 warnings in 4.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 5, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -286,7 +286,7 @@
set_span_ok(span)
result = dict(NO_ACTION)
- if action == "opened":
+ if action is "opened":
# This only produces an operator warning, so it runs after the
# review decision instead of delaying it.
await self._warn_missing_approval_labels(event)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......F
=================================== FAILURES ===================================
__________________ test_pr_opened_missing_label_logs_warning ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f480b1e75b0>
mock_github_client = <MagicMock name='github_client' id='139947396631920'>
@pytest.mark.asyncio
async def test_pr_opened_missing_label_logs_warning(webhook_handler, mock_github_client):
"""Test missing approval label triggers label check after the review decision."""
payload = load_fixture("pr_opened_with_autoapprove_label")
mock_github_client.repo_has_label.return_value = False
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
# The label on the pull request exists by definition; only "stamp" is checked.
> mock_github_client.repo_has_label.assert_called_once_with(12345, "octocat/hello-world", "stamp")
tests/test_webhook_handler.py:352:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.repo_has_label' id='139947396636288'>
args = (12345, 'octocat/hello-world', 'stamp'), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:48:12.222426Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:48:12.223000Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:48:12.223255Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:48:12.222426Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:48:12.223000Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:48:12.223255Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:289
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:289: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if action is "opened":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_missing_label_logs_warning - AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 367 passed, 3 deselected, 2 warnings in 4.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 6, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -306,7 +306,7 @@
if not config.auto_approve_on_label:
return Decision.NONE, None
- if event.action == "unlabeled":
+ if event.action is "unlabeled":
if event.event_label in config.approval_labels:
return Decision.DISMISS, event.event_label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe89b36caa0>
mock_github_client = <MagicMock name='github_client' id='140637002922176'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "error"
E AssertionError: assert 'ignored' == 'error'
E
E - error
E + ignored
tests/test_webhook_handler.py:210: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:45:52.348551Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:45:52.349072Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:45:52.348551Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:45:52.349072Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:309
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:309: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if event.action is "unlabeled":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - AssertionError: assert 'ignored' == 'error'
- error
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 2 warnings in 3.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 7, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -322,7 +322,7 @@
# any duplicate afterwards.
return Decision.APPROVE, label
- if event.action == "labeled":
+ if event.action is "labeled":
if event.event_label in config.approval_labels:
return Decision.APPROVE, label
return Decision.REFRESH, label........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6e47ec5940>
mock_github_client = <MagicMock name='github_client' id='140111623238192'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
> assert [r["status"] for r in results] == ["success", "success"]
E AssertionError: assert ['success', 'ignored'] == ['success', 'success']
E
E At index 1 diff: 'ignored' != 'success'
E
E Full diff:
E [
E 'success',
E - 'success',
E + 'ignored',
E ]
tests/test_webhook_handler.py:116: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:36:40.962977Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:36:40.963606Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:36:40.963880Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:36:40.964544Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:36:40.962977Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:36:40.963606Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:36:40.963880Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:36:40.964544Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:325
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:325: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if event.action is "labeled":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: assert ['success', 'ignored'] == ['success', 'success']
At index 1 diff: 'ignored' != 'success'
Full diff:
[
'success',
- 'success',
+ 'ignored',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 2 warnings in 4.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 8, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -327,7 +327,7 @@
return Decision.APPROVE, label
return Decision.REFRESH, label
- if event.action == "synchronize" and config.reapprove:
+ if event.action is "synchronize" and config.reapprove:
return Decision.REFRESH, label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:330
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:330: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if event.action is "synchronize" and config.reapprove:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 9, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -586,7 +586,7 @@
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
if proven_only:
- return bool(head_sha) and commit == head_sha
+ return bool(head_sha) and commit is head_sha
return not head_sha or not commit or commit == head_sha
return sorted(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7efc190088d0>
mock_github_client = <MagicMock name='github_client' id='139621215931424'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='139621215932096'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:38:02.412380Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:38:02.412985Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:38:02.413251Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:38:02.412380Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:38:02.412985Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:38:02.413251Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.29s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 10, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha or not commit or commit is head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7faf8a4f9940>
mock_github_client = <MagicMock name='github_client' id='140391910912560'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140391910910880'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:40:56.412500Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:40:56.413111Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:40:56.413398Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:40:56.414077Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:40:56.414330Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:40:56.412500Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:40:56.413111Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:40:56.413398Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:40:56.414077Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:40:56.414330Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.18s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 11, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -592,7 +592,7 @@
return sorted(
review["id"]
for review in reviews
- if review["state"] == "APPROVED" and covers_head(review)
+ if review["state"] is "APPROVED" and covers_head(review)
)
def _has_current_active_approval(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:595
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:595: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if review["state"] is "APPROVED" and covers_head(review)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 12, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -704,7 +704,7 @@
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
- if command == "help":
+ if command is "help":
success = await self._post_help(
installation_id,
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...........................................................F
=================================== FAILURES ===================================
_______________________ test_issue_comment_help_command ________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa417a60050>
mock_github_client = <MagicMock name='github_client' id='140342745858800'>
@pytest.mark.asyncio
async def test_issue_comment_help_command(webhook_handler, mock_github_client):
"""Test @stampbot help posts contextual help."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:974: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:59:09.270309Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:59:09.270906Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:59:09.270309Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:59:09.270906Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:707
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:707: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if command is "help":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_command - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 419 passed, 3 deselected, 2 warnings in 4.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 0, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -160,7 +160,7 @@
)
# Route to appropriate handler
- if event_type == "pull_request":
+ if event_type is not "pull_request":
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f867ebe19d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'error' == 'ok'
E
E - ok
E + error
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:08:12.392997Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in PR event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:08:12.393644Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:08:12.394447Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:08:12.392997Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:212 {'event': 'Missing required fields in PR event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:08:12.393644Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:163
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:163: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if event_type is not "pull_request":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'error' == 'ok'
- ok
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 2 warnings in 3.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 1, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -162,7 +162,7 @@
# Route to appropriate handler
if event_type == "pull_request":
result = await self._handle_pull_request(payload)
- elif event_type == "pull_request_review_comment":
+ elif event_type is not "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
elif event_type == "issue_comment":
# Issue comments can be on PRs too........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f3c350e59d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:35:36.508985Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:35:36.510346Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:35:36.508985Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:165
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:165: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
elif event_type is not "pull_request_review_comment":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 2 warnings in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 2, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -164,7 +164,7 @@
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)
- elif event_type == "issue_comment":
+ elif event_type is not "issue_comment":
# Issue comments can be on PRs too
if "pull_request" in payload.get("issue", {}):
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f3cf1df19d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:44:32.724831Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:44:32.726299Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:44:32.724831Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:167
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:167: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
elif event_type is not "issue_comment":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 2 warnings in 3.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 3, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -170,7 +170,7 @@
result = await self._handle_pr_comment(payload)
else:
result = {"status": "ignored", "message": "Not a PR comment"}
- elif event_type == "ping":
+ elif event_type is not "ping":
result = {"status": "ok", "message": "pong"}
else:
result = {"status": "ignored", "message": f"Event type {event_type} not handled"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.......................................................F
=================================== FAILURES ===================================
_______________________________ test_ping_event ________________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7ff6a734a350>
@pytest.mark.asyncio
async def test_ping_event(webhook_handler):
"""Test handling ping event."""
payload = {"zen": "Design for failure."}
result = await webhook_handler.handle_event("ping", payload)
> assert result["status"] == "ok"
E AssertionError: assert 'ignored' == 'ok'
E
E - ok
E + ignored
tests/test_webhook_handler.py:53: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:53:43.072840Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:53:43.072840Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:173
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:173: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
elif event_type is not "ping":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_ping_event - AssertionError: assert 'ignored' == 'ok'
- ok
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 343 passed, 3 deselected, 2 warnings in 4.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 4, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -244,7 +244,7 @@
},
)
- if action == "opened":
+ if action is not "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
.......F
=================================== FAILURES ===================================
_______________ test_invalid_repo_config_no_review_on_non_opened _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc16beb3bf0>
mock_github_client = <MagicMock name='github_client' id='140468711484656'>
@pytest.mark.asyncio
async def test_invalid_repo_config_no_review_on_non_opened(webhook_handler, mock_github_client):
"""Test invalid config skips review comment for non-opened events."""
payload = load_fixture("pr_opened_with_autoapprove_label")
payload["action"] = "labeled"
mock_github_client.get_repo_file.return_value = 'chatops_required_permission = "invalid"'
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "error"
> mock_github_client.create_pr_review_comment.assert_not_called()
tests/test_webhook_handler.py:1364:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.create_pr_review_comment' id='140468711488352'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'create_pr_review_comment' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Stampbot configuration error:\n\nInvalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin\n\nPlease fix the configuration to re-enable automation.')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:41:18.388229Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin"}, "event": "Invalid Stampbot configuration for octocat/hello-world: Invalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:41:18.388877Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:41:18.388229Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin'}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: Invalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:41:18.388877Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:247
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:247: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if action is not "opened":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_invalid_repo_config_no_review_on_non_opened - AssertionError: Expected 'create_pr_review_comment' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Stampbot configuration error:\n\nInvalid chatops_required_permission: invalid. Valid values: none, read, triage, write, maintain, admin\n\nPlease fix the configuration to re-enable automation.')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 439 passed, 3 deselected, 2 warnings in 5.35s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 5, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -286,7 +286,7 @@
set_span_ok(span)
result = dict(NO_ACTION)
- if action == "opened":
+ if action is not "opened":
# This only produces an operator warning, so it runs after the
# review decision instead of delaying it.
await self._warn_missing_approval_labels(event)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:289
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:289: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if action is not "opened":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 4.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 6, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -306,7 +306,7 @@
if not config.auto_approve_on_label:
return Decision.NONE, None
- if event.action == "unlabeled":
+ if event.action is not "unlabeled":
if event.event_label in config.approval_labels:
return Decision.DISMISS, event.event_label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f49d06f35c0>
mock_github_client = <MagicMock name='github_client' id='139955000981024'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:55:16.552074Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:55:16.552913Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:55:16.552074Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:55:16.552913Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:309
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:309: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if event.action is not "unlabeled":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 2 warnings in 4.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 7, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -322,7 +322,7 @@
# any duplicate afterwards.
return Decision.APPROVE, label
- if event.action == "labeled":
+ if event.action is not "labeled":
if event.event_label in config.approval_labels:
return Decision.APPROVE, label
return Decision.REFRESH, label........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
......................F
=================================== FAILURES ===================================
____________________ test_pr_synchronize_reapprove_disabled ____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7ff05312bd70>
mock_github_client = <MagicMock name='github_client' id='140670168645120'>
@pytest.mark.asyncio
async def test_pr_synchronize_reapprove_disabled(webhook_handler, mock_github_client):
"""Test new commits do not re-approve by default."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "ignored"
assert "no action" in result["message"].lower()
> mock_github_client.find_bot_approval_reviews.assert_not_called()
tests/test_webhook_handler.py:531:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='140670165869504'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'find_bot_approval_reviews' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:04:01.996279Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:04:01.996887Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:04:01.996279Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:04:01.996887Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:325
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:325: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if event.action is not "labeled":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_reapprove_disabled - AssertionError: Expected 'find_bot_approval_reviews' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 382 passed, 3 deselected, 2 warnings in 4.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 8, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -327,7 +327,7 @@
return Decision.APPROVE, label
return Decision.REFRESH, label
- if event.action == "synchronize" and config.reapprove:
+ if event.action is not "synchronize" and config.reapprove:
return Decision.REFRESH, label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb7191de210>
mock_github_client = <MagicMock name='github_client' id='140424219801456'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:48:18.912441Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:48:18.912441Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:330
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:330: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if event.action is not "synchronize" and config.reapprove:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 2 warnings in 4.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 9, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -586,7 +586,7 @@
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
if proven_only:
- return bool(head_sha) and commit == head_sha
+ return bool(head_sha) and commit is not head_sha
return not head_sha or not commit or commit == head_sha
return sorted(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb93c1e08d0>
mock_github_client = <MagicMock name='github_client' id='140433484876832'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140433484877504'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
E call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:00:49.535729Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:00:49.536276Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:00:49.536545Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:00:49.535729Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:00:49.536276Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:00:49.536545Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 10, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha or not commit or commit is not head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...................................................................F
=================================== FAILURES ===================================
___________ test_active_approvals_for_head[reviews1-head-expected1] ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fce401ab5b0>
reviews = [{'id': 1, 'state': 'APPROVED', 'commit_id': 'head'}]
head_sha = 'head', expected = [1]
@pytest.mark.parametrize(
("reviews", "head_sha", "expected"),
[
([], "head", []),
([{"id": 1, "state": "APPROVED", "commit_id": "head"}], "head", [1]),
(
[
{"id": 3, "state": "APPROVED", "commit_id": "head"},
{"id": 1, "state": "APPROVED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[1, 2, 3],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": "old"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": None},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
None,
[1, 2],
),
(
[
{"id": 1, "state": "DISMISSED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
],
)
def test_active_approvals_for_head(webhook_handler, reviews, head_sha, expected):
"""Test which approvals count as covering the head, oldest first."""
> assert webhook_handler._active_approvals_for_head(reviews, head_sha) == expected
E assert [] == [1]
E
E Right contains one more item: 1
E
E Full diff:
E + []
E - [
E - 1,
E - ]
tests/test_webhook_handler.py:256: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_active_approvals_for_head[reviews1-head-expected1] - assert [] == [1]
Right contains one more item: 1
Full diff:
+ []
- [
- 1,
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 355 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 11, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -592,7 +592,7 @@
return sorted(
review["id"]
for review in reviews
- if review["state"] == "APPROVED" and covers_head(review)
+ if review["state"] is not "APPROVED" and covers_head(review)
)
def _has_current_active_approval(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa33fe3d940>
mock_github_client = <MagicMock name='github_client' id='140339123401264'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140339123399248'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:54:53.304772Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:54:53.305341Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:54:53.305632Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:54:53.306295Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:54:53.306571Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:54:53.304772Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:54:53.305341Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:54:53.305632Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:54:53.306295Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:54:53.306571Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:595
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:595: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if review["state"] is not "APPROVED" and covers_head(review)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 2 warnings in 4.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 12, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -704,7 +704,7 @@
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
- if command == "help":
+ if command is not "help":
success = await self._post_help(
installation_id,
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..................................................F
=================================== FAILURES ===================================
__________________________ test_issue_comment_approve __________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f964c630290>
mock_github_client = <MagicMock name='github_client' id='140283504152928'>
@pytest.mark.asyncio
async def test_issue_comment_approve(webhook_handler, mock_github_client):
"""Test @stampbot approve command."""
payload = load_fixture("issue_comment_approve")
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> assert "approved" in result["message"].lower()
E AssertionError: assert 'approved' in 'help message posted'
E + where 'help message posted' = <built-in method lower of str object at 0x7f964cf1f3f0>()
E + where <built-in method lower of str object at 0x7f964cf1f3f0> = 'Help message posted'.lower
tests/test_webhook_handler.py:830: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:19:26.584538Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:19:26.585092Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:19:26.584538Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:19:26.585092Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/webhook_handler.py:707
/home/runner/work/stampbot/stampbot/stampbot/webhook_handler.py:707: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if command is not "help":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_approve - AssertionError: assert 'approved' in 'help message posted'
+ where 'help message posted' = <built-in method lower of str object at 0x7f964cf1f3f0>()
+ where <built-in method lower of str object at 0x7f964cf1f3f0> = 'Help message posted'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 410 passed, 3 deselected, 2 warnings in 4.51s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -624,7 +624,7 @@
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
- if len(comment_body) > MAX_COMMENT_LENGTH:
+ if len(comment_body) == MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.........................................................F
=================================== FAILURES ===================================
_____________________ test_issue_comment_too_long_rejected _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f974c2b10d0>
mock_github_client = <MagicMock name='github_client' id='140287794481872'>
@pytest.mark.asyncio
async def test_issue_comment_too_long_rejected(webhook_handler, mock_github_client):
"""Test that comments exceeding max length are rejected to prevent DoS."""
payload = load_fixture("issue_comment_approve")
# Create an excessively long comment (> 64KB)
payload["comment"]["body"] = "@stampbot approve " + "x" * 100000
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "ignored"
E AssertionError: assert 'success' == 'ignored'
E
E - ignored
E + success
tests/test_webhook_handler.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:02:45.690068Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:02:45.690740Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:02:45.690068Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:02:45.690740Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_too_long_rejected - AssertionError: assert 'success' == 'ignored'
- ignored
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 417 passed, 3 deselected, 1 warning in 4.73s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -624,7 +624,7 @@
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
- if len(comment_body) > MAX_COMMENT_LENGTH:
+ if len(comment_body) != MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbb0b0faa80>
mock_github_client = <MagicMock name='github_client' id='140441313129008'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:59:26.043627Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:59:26.043627Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -624,7 +624,7 @@
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
- if len(comment_body) > MAX_COMMENT_LENGTH:
+ if len(comment_body) < MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fed83ded6d0>
mock_github_client = <MagicMock name='github_client' id='140658089783856'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:40.716031Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:40.716031Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -624,7 +624,7 @@
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
- if len(comment_body) > MAX_COMMENT_LENGTH:
+ if len(comment_body) <= MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f109a8ec200>
mock_github_client = <MagicMock name='github_client' id='139709283119664'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:51:57.045091Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:51:57.045091Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.24s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -624,7 +624,7 @@
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
- if len(comment_body) > MAX_COMMENT_LENGTH:
+ if len(comment_body) >= MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -624,7 +624,7 @@
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
- if len(comment_body) > MAX_COMMENT_LENGTH:
+ if len(comment_body) is MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.........................................................F
=================================== FAILURES ===================================
_____________________ test_issue_comment_too_long_rejected _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6fcfd3ff50>
mock_github_client = <MagicMock name='github_client' id='140118202092240'>
@pytest.mark.asyncio
async def test_issue_comment_too_long_rejected(webhook_handler, mock_github_client):
"""Test that comments exceeding max length are rejected to prevent DoS."""
payload = load_fixture("issue_comment_approve")
# Create an excessively long comment (> 64KB)
payload["comment"]["body"] = "@stampbot approve " + "x" * 100000
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "ignored"
E AssertionError: assert 'success' == 'ignored'
E
E - ignored
E + success
tests/test_webhook_handler.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:49:25.588755Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:49:25.589340Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:49:25.588755Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:49:25.589340Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_too_long_rejected - AssertionError: assert 'success' == 'ignored'
- ignored
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 417 passed, 3 deselected, 1 warning in 4.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -624,7 +624,7 @@
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
- if len(comment_body) > MAX_COMMENT_LENGTH:
+ if len(comment_body) is not MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7efe0d8e0200>
mock_github_client = <MagicMock name='github_client' id='139629606924848'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:49:39.185592Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:49:39.185592Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_Eq, occurrence: 0, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -275,7 +275,7 @@
)
decision, label = self._decide(event)
- if decision is Decision.APPROVE and label:
+ if decision == Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_Eq, occurrence: 1, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -277,7 +277,7 @@
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
- elif decision is Decision.REFRESH and label:
+ elif decision == Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
elif decision is Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_Eq, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -279,7 +279,7 @@
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
- elif decision is Decision.DISMISS and label:
+ elif decision == Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)
else:
add_span_attributes(span, {"webhook.result": "no_action"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.25s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_Eq, occurrence: 3, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -531,7 +531,7 @@
event.repo_full_name,
label,
)
- if label_exists is False:
+ if label_exists == False:
logger.warning(
"Approval label %s not found in %s",
label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_NotEq, occurrence: 0, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -275,7 +275,7 @@
)
decision, label = self._decide(event)
- if decision is Decision.APPROVE and label:
+ if decision != Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbb94dfb5c0>
mock_github_client = <MagicMock name='github_client' id='140443627697696'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:51:03.208628Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:51:03.209438Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:51:03.208628Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:51:03.209438Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_NotEq, occurrence: 1, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -277,7 +277,7 @@
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
- elif decision is Decision.REFRESH and label:
+ elif decision != Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
elif decision is Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f75fe47fa70>
mock_github_client = <MagicMock name='github_client' id='140144695998320'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:59:27.904640Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:59:27.904640Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_NotEq, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -279,7 +279,7 @@
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
- elif decision is Decision.DISMISS and label:
+ elif decision != Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)
else:
add_span_attributes(span, {"webhook.result": "no_action"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f71581fe990>
mock_github_client = <MagicMock name='github_client' id='140124697147584'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "error"
E AssertionError: assert 'ignored' == 'error'
E
E - error
E + ignored
tests/test_webhook_handler.py:210: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:41:04.115778Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:41:04.116409Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:41:04.115778Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:41:04.116409Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - AssertionError: assert 'ignored' == 'error'
- error
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.33s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_NotEq, occurrence: 3, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -531,7 +531,7 @@
event.repo_full_name,
label,
)
- if label_exists is False:
+ if label_exists != False:
logger.warning(
"Approval label %s not found in %s",
label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Lt, occurrence: 0, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -275,7 +275,7 @@
)
decision, label = self._decide(event)
- if decision is Decision.APPROVE and label:
+ if decision < Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f96bc5fb5c0>
mock_github_client = <MagicMock name='github_client' id='140285307303456'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f96bc5fb5c0>
payload = {'action': 'opened', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pulls/42'...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
> if decision < Decision.APPROVE and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '<' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:278: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:45:10.684068Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:45:10.684901Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:45:10.684068Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:45:10.684901Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: '<' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Lt, occurrence: 1, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -277,7 +277,7 @@
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
- elif decision is Decision.REFRESH and label:
+ elif decision < Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
elif decision is Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6de48028a0>
mock_github_client = <MagicMock name='github_client' id='140109872117616'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:196:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f6de48028a0>
payload = {'action': 'synchronize', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pull...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
> elif decision < Decision.REFRESH and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '<' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:280: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:37:38.043605Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:37:38.043605Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - TypeError: '<' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.27s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Lt, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -279,7 +279,7 @@
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
- elif decision is Decision.DISMISS and label:
+ elif decision < Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)
else:
add_span_attributes(span, {"webhook.result": "no_action"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa4040ee990>
mock_github_client = <MagicMock name='github_client' id='140342330013888'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:208:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fa4040ee990>
payload = {'action': 'unlabeled', 'number': 42, 'label': {'id': 111111, 'node_id': 'LA_test1', 'url': 'https://api.github.com/re...', 'id': 123456789, 'node_id': 'PR_kwDOBtest', 'html_url': 'https://github.com/octocat/hello-world/pull/42', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
> elif decision < Decision.DISMISS and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '<' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:282: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:13:30.501729Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:13:30.502256Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:13:30.501729Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:13:30.502256Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - TypeError: '<' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_Lt, occurrence: 3, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -531,7 +531,7 @@
event.repo_full_name,
label,
)
- if label_exists is False:
+ if label_exists < False:
logger.warning(
"Approval label %s not found in %s",
label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_LtE, occurrence: 0, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -275,7 +275,7 @@
)
decision, label = self._decide(event)
- if decision is Decision.APPROVE and label:
+ if decision <= Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f4b851e35c0>
mock_github_client = <MagicMock name='github_client' id='139962326316576'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f4b851e35c0>
payload = {'action': 'opened', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pulls/42'...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
> if decision <= Decision.APPROVE and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '<=' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:278: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:57.995355Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:29:57.996222Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:57.995355Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:29:57.996222Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: '<=' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_LtE, occurrence: 1, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -277,7 +277,7 @@
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
- elif decision is Decision.REFRESH and label:
+ elif decision <= Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
elif decision is Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb6ddda7a70>
mock_github_client = <MagicMock name='github_client' id='140423324612464'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:196:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fb6ddda7a70>
payload = {'action': 'synchronize', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pull...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
> elif decision <= Decision.REFRESH and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '<=' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:280: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:49:18.058631Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:49:18.058631Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - TypeError: '<=' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.25s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_LtE, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -279,7 +279,7 @@
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
- elif decision is Decision.DISMISS and label:
+ elif decision <= Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)
else:
add_span_attributes(span, {"webhook.result": "no_action"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1d0c699b80>
mock_github_client = <MagicMock name='github_client' id='139762594478272'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:208:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f1d0c699b80>
payload = {'action': 'unlabeled', 'number': 42, 'label': {'id': 111111, 'node_id': 'LA_test1', 'url': 'https://api.github.com/re...', 'id': 123456789, 'node_id': 'PR_kwDOBtest', 'html_url': 'https://github.com/octocat/hello-world/pull/42', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
> elif decision <= Decision.DISMISS and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '<=' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:282: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:58:34.306767Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:58:34.307308Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:58:34.306767Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:58:34.307308Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - TypeError: '<=' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_LtE, occurrence: 3, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -531,7 +531,7 @@
event.repo_full_name,
label,
)
- if label_exists is False:
+ if label_exists <= False:
logger.warning(
"Approval label %s not found in %s",
label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Gt, occurrence: 0, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -275,7 +275,7 @@
)
decision, label = self._decide(event)
- if decision is Decision.APPROVE and label:
+ if decision > Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fdb7daf75c0>
mock_github_client = <MagicMock name='github_client' id='140580677613088'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fdb7daf75c0>
payload = {'action': 'opened', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pulls/42'...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
> if decision > Decision.APPROVE and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '>' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:278: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:18:18.870836Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:18:18.871668Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:18:18.870836Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:18:18.871668Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: '>' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Gt, occurrence: 1, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -277,7 +277,7 @@
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
- elif decision is Decision.REFRESH and label:
+ elif decision > Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
elif decision is Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6e3c177a70>
mock_github_client = <MagicMock name='github_client' id='140111373858672'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:196:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f6e3c177a70>
payload = {'action': 'synchronize', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pull...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
> elif decision > Decision.REFRESH and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '>' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:280: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:03:06.266610Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:03:06.266610Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - TypeError: '>' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Gt, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -279,7 +279,7 @@
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
- elif decision is Decision.DISMISS and label:
+ elif decision > Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)
else:
add_span_attributes(span, {"webhook.result": "no_action"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fddeb2f8aa0>
mock_github_client = <MagicMock name='github_client' id='140591097837760'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:208:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fddeb2f8aa0>
payload = {'action': 'unlabeled', 'number': 42, 'label': {'id': 111111, 'node_id': 'LA_test1', 'url': 'https://api.github.com/re...', 'id': 123456789, 'node_id': 'PR_kwDOBtest', 'html_url': 'https://github.com/octocat/hello-world/pull/42', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
> elif decision > Decision.DISMISS and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '>' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:282: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:27:13.323641Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:27:13.324169Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:27:13.323641Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:27:13.324169Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - TypeError: '>' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.16s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_Gt, occurrence: 3, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -531,7 +531,7 @@
event.repo_full_name,
label,
)
- if label_exists is False:
+ if label_exists > False:
logger.warning(
"Approval label %s not found in %s",
label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_GtE, occurrence: 0, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -275,7 +275,7 @@
)
decision, label = self._decide(event)
- if decision is Decision.APPROVE and label:
+ if decision >= Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2ef9e1f5c0>
mock_github_client = <MagicMock name='github_client' id='139839731168800'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f2ef9e1f5c0>
payload = {'action': 'opened', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pulls/42'...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
> if decision >= Decision.APPROVE and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '>=' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:278: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:30:43.131894Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:30:43.132729Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:30:43.131894Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:30:43.132729Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: '>=' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_GtE, occurrence: 1, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -277,7 +277,7 @@
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
- elif decision is Decision.REFRESH and label:
+ elif decision >= Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
elif decision is Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa9b3abfa70>
mock_github_client = <MagicMock name='github_client' id='140366782855024'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:196:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fa9b3abfa70>
payload = {'action': 'synchronize', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pull...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
> elif decision >= Decision.REFRESH and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '>=' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:280: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:19:43.413744Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:19:43.413744Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - TypeError: '>=' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_GtE, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -279,7 +279,7 @@
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
- elif decision is Decision.DISMISS and label:
+ elif decision >= Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)
else:
add_span_attributes(span, {"webhook.result": "no_action"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc236536990>
mock_github_client = <MagicMock name='github_client' id='140472021432512'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:208:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fc236536990>
payload = {'action': 'unlabeled', 'number': 42, 'label': {'id': 111111, 'node_id': 'LA_test1', 'url': 'https://api.github.com/re...', 'id': 123456789, 'node_id': 'PR_kwDOBtest', 'html_url': 'https://github.com/octocat/hello-world/pull/42', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
> elif decision >= Decision.DISMISS and label:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: '>=' not supported between instances of 'Decision' and 'Decision'
stampbot/webhook_handler.py:282: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:30:04.313797Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:30:04.314324Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:30:04.313797Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:30:04.314324Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - TypeError: '>=' not supported between instances of 'Decision' and 'Decision'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_GtE, occurrence: 3, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -531,7 +531,7 @@
event.repo_full_name,
label,
)
- if label_exists is False:
+ if label_exists >= False:
logger.warning(
"Approval label %s not found in %s",
label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 0, definition_name: webhook_secret
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -93,7 +93,7 @@
Raises:
RuntimeError: If webhook secret is not configured.
"""
- if self._webhook_secret is None:
+ if self._webhook_secret is not None:
if not settings.webhook_secret:
raise RuntimeError(
"Webhook secret not configured. Visit /setup to create your GitHub App."........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f5d960a7680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:118: in verify_signature
"sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/hmac.py:218: in new
return HMAC(key, msg, digestmod)
^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <hmac.HMAC object at 0x7f5d95c72980>, key = None
msg = b'{"test": "data"}', digestmod = <built-in function openssl_sha256>
def __init__(self, key, msg=None, digestmod=''):
"""Create a new HMAC object.
key: bytes or buffer, key for the keyed hash object.
msg: bytes or buffer, Initial input for the hash or None.
digestmod: A hash name suitable for hashlib.new(). *OR*
A hashlib constructor returning a new hash object. *OR*
A module supporting PEP 247.
Required as of 3.8, despite its position after the optional
msg argument. Passing it as a keyword argument is
recommended, though not required for legacy API reasons.
"""
if not isinstance(key, (bytes, bytearray)):
> raise TypeError(f"key: expected bytes or bytearray, "
f"but got {type(key).__name__!r}")
E TypeError: key: expected bytes or bytearray, but got 'NoneType'
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/hmac.py:71: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: key: expected bytes or bytearray, but got 'NoneType'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 1, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -275,7 +275,7 @@
)
decision, label = self._decide(event)
- if decision is Decision.APPROVE and label:
+ if decision is not Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f5d297375c0>
mock_github_client = <MagicMock name='github_client' id='140038099351072'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:20:08.077185Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:20:08.077988Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:20:08.077185Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:20:08.077988Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 3.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -277,7 +277,7 @@
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
- elif decision is Decision.REFRESH and label:
+ elif decision is not Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
elif decision is Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1dd0922990>
mock_github_client = <MagicMock name='github_client' id='139765940369264'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:22:00.336989Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:22:00.336989Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 3, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -279,7 +279,7 @@
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
- elif decision is Decision.DISMISS and label:
+ elif decision is not Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)
else:
add_span_attributes(span, {"webhook.result": "no_action"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbcc2f86990>
mock_github_client = <MagicMock name='github_client' id='140448617789632'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "error"
E AssertionError: assert 'ignored' == 'error'
E
E - error
E + ignored
tests/test_webhook_handler.py:210: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:44:11.331505Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:44:11.332065Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:44:11.331505Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:44:11.332065Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - AssertionError: assert 'ignored' == 'error'
- error
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 4, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -312,7 +312,7 @@
return Decision.NONE, None
label = self._find_approval_label(event.labels, config)
- if label is None:
+ if label is not None:
return Decision.NONE, None
if event.action in ("opened", "reopened"):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbe8c1e75c0>
mock_github_client = <MagicMock name='github_client' id='140456296805920'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:02:59.042686Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:02:59.043549Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:02:59.042686Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:02:59.043549Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 5, definition_name: _refresh_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -366,7 +366,7 @@
event.repo_full_name,
event.pr_number,
)
- if reviews is None:
+ if reviews is not None:
# GitHub could not be read. Approving may create a duplicate that
# the cleanup removes; skipping could miss an approval for good.
logger.warning(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f3d6f823a70>
mock_github_client = <MagicMock name='github_client' id='139901783373680'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:56:05.549891Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:56:05.549891Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 6, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -531,7 +531,7 @@
event.repo_full_name,
label,
)
- if label_exists is False:
+ if label_exists is not False:
logger.warning(
"Approval label %s not found in %s",
label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 7, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -959,7 +959,7 @@
repo_full_name,
pr_number,
)
- if reviews is None:
+ if reviews is not None:
# Unknown state. A duplicate is recoverable; a missed
# approval is not, so approve and let the cleanup sort it out.
logger.warning(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f22f9051940>
mock_github_client = <MagicMock name='github_client' id='139788176962096'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='139788176960080'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:42:53.917134Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:42:53.917750Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:42:53.918023Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42}, "event": "Could not read reviews for PR #42 in octocat/hello-world; approving without a check", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:42:53.918270Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:42:53.918809Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:42:53.919061Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42}, "event": "Could not read reviews for PR #42 in octocat/hello-world; approving without a check", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:42:53.919278Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:42:53.917134Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:42:53.917750Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:42:53.918023Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:965 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42}, 'event': 'Could not read reviews for PR #42 in octocat/hello-world; approving without a check', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:42:53.918270Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:42:53.918809Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:42:53.919061Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:965 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42}, 'event': 'Could not read reviews for PR #42 in octocat/hello-world; approving without a check', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:42:53.919278Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 8, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1075,7 +1075,7 @@
repo_full_name,
pr_number,
)
- if reviews is None:
+ if reviews is not None:
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f37e0abdbf0>
mock_github_client = <MagicMock name='github_client' id='139877967219744'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='139877967220416'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:59:01.750314Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:59:01.750888Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:59:01.751146Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "Stampbot reviews could not be listed"}, "event": "Error dismissing approvals: Stampbot reviews could not be listed", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T20:59:01.751640Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:59:01.750314Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:59:01.750888Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:59:01.751146Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': 'Stampbot reviews could not be listed'}, 'event': 'Error dismissing approvals: Stampbot reviews could not be listed', 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T20:59:01.751640Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_UAdd, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -637,7 +637,7 @@
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
- pr_number = int(pr_url.split("/")[-1])
+ pr_number = int(pr_url.split("/")[+1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f92b92f0200>
mock_github_client = <MagicMock name='github_client' id='140268143014448'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f92b92f0200>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
> pr_number = int(pr_url.split("/")[+1])
^^^^^^^^^^^^^^^^^^^^^^^^^^
E ValueError: invalid literal for int() with base 10: ''
stampbot/webhook_handler.py:640: ValueError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:43:30.805348Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:43:30.805348Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - ValueError: invalid literal for int() with base 10: ''
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Invert, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -637,7 +637,7 @@
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
- pr_number = int(pr_url.split("/")[-1])
+ pr_number = int(pr_url.split("/")[~1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f3e839f0200>
mock_github_client = <MagicMock name='github_client' id='139906466853424'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f3e839f0200>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
> pr_number = int(pr_url.split("/")[~1])
^^^^^^^^^^^^^^^^^^^^^^^^^^
E ValueError: invalid literal for int() with base 10: 'pulls'
stampbot/webhook_handler.py:640: ValueError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:41:25.201186Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:41:25.201186Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - ValueError: invalid literal for int() with base 10: 'pulls'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.26s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Not, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -637,7 +637,7 @@
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
- pr_number = int(pr_url.split("/")[-1])
+ pr_number = int(pr_url.split("/")[not 1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f022e5f0200>
mock_github_client = <MagicMock name='github_client' id='139647337689648'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f022e5f0200>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
> pr_number = int(pr_url.split("/")[not 1])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E ValueError: invalid literal for int() with base 10: 'https:'
stampbot/webhook_handler.py:640: ValueError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:27:36.980100Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:27:36.980100Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - ValueError: invalid literal for int() with base 10: 'https:'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_USub, occurrence: 0, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -637,7 +637,7 @@
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
- pr_number = int(pr_url.split("/")[-1])
+ pr_number = int(pr_url.split("/")[1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f478daf0200>
mock_github_client = <MagicMock name='github_client' id='139945291002416'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f478daf0200>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
> pr_number = int(pr_url.split("/")[1])
^^^^^^^^^^^^^^^^^^^^^^^^^
E ValueError: invalid literal for int() with base 10: ''
stampbot/webhook_handler.py:640: ValueError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:15:55.431673Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:15:55.431673Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - ValueError: invalid literal for int() with base 10: ''
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 0, definition_name: webhook_secret
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -94,7 +94,7 @@
RuntimeError: If webhook secret is not configured.
"""
if self._webhook_secret is None:
- if not settings.webhook_secret:
+ if settings.webhook_secret:
raise RuntimeError(
"Webhook secret not configured. Visit /setup to create your GitHub App."
)........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f0c00d7f680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:118: in verify_signature
"sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f0c012e5010>
@property
def webhook_secret(self) -> bytes:
"""Get webhook secret, initializing if needed.
Returns:
Webhook secret as bytes.
Raises:
RuntimeError: If webhook secret is not configured.
"""
if self._webhook_secret is None:
if settings.webhook_secret:
> raise RuntimeError(
"Webhook secret not configured. Visit /setup to create your GitHub App."
)
E RuntimeError: Webhook secret not configured. Visit /setup to create your GitHub App.
stampbot/webhook_handler.py:98: RuntimeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - RuntimeError: Webhook secret not configured. Visit /setup to create your GitHub App.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 1, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -111,7 +111,7 @@
Returns:
True if signature is valid
"""
- if not signature:
+ if signature:
return False
expected_signature = (........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.F
=================================== FAILURES ===================================
__________________________ test_webhook_invalid_json ___________________________
test_client = <starlette.testclient.TestClient object at 0x7f68317d0270>
def test_webhook_invalid_json(test_client: TestClient):
"""Test webhook rejects invalid JSON payload."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
# Create a valid signature for invalid JSON
body = b"not valid json"
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 400
E assert 401 == 400
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:330: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T20:59:59.148434Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:59:59.149215Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T20:59:59.148434Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_invalid_json - assert 401 == 400
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 217 passed, 3 deselected, 1 warning in 3.53s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -208,7 +208,7 @@
"github.action": action or "unknown",
},
) as span:
- if not all([pr_number, repo_full_name, installation_id]):
+ if all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f27c524b5c0>
mock_github_client = <MagicMock name='github_client' id='139808783398432'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:12:43.953172Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in PR event", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:12:43.953310Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:12:43.953172Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:212 {'event': 'Missing required fields in PR event', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:12:43.953310Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 3, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -303,7 +303,7 @@
The decision and the approval label it concerns, if any.
"""
config = event.repo_config
- if not config.auto_approve_on_label:
+ if config.auto_approve_on_label:
return Decision.NONE, None
if event.action == "unlabeled":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1850b2b5c0>
mock_github_client = <MagicMock name='github_client' id='139742405064224'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:43:29.212422Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:43:29.213285Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:43:29.212422Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:43:29.213285Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 4, definition_name: _refresh_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -375,7 +375,7 @@
event.repo_full_name,
extra={"repo": event.repo_full_name, "pr_number": event.pr_number},
)
- elif not self._approval_needs_refresh(reviews, event.head_sha):
+ elif self._approval_needs_refresh(reviews, event.head_sha):
add_span_attributes(span, {"webhook.result": "no_action"})
set_span_ok(span)
return dict(NO_ACTION)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
....................F
=================================== FAILURES ===================================
_________ test_pr_labeled_unrelated_label_with_approval_label_ignored __________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f12e58283b0>
mock_github_client = <MagicMock name='github_client' id='139719136617408'>
@pytest.mark.asyncio
async def test_pr_labeled_unrelated_label_with_approval_label_ignored(
webhook_handler, mock_github_client
):
"""Test unrelated label changes do not re-approve just because an approval label exists."""
payload = load_fixture("pr_labeled_autoapprove")
payload["label"] = {"name": "bug"}
payload["pull_request"]["labels"].append({"name": "bug"})
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "ignored"
E AssertionError: assert 'success' == 'ignored'
E
E - ignored
E + success
tests/test_webhook_handler.py:496: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:04:35.577229Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:04:35.577788Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:04:35.578156Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:04:35.577229Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:04:35.577788Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:04:35.578156Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_unrelated_label_with_approval_label_ignored - AssertionError: assert 'success' == 'ignored'
- ignored
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 380 passed, 3 deselected, 1 warning in 4.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 5, definition_name: _approve_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -420,7 +420,7 @@
pr_author,
author_team_slugs,
)
- if not is_eligible:
+ if is_eligible:
logger.info(
"PR #%d not eligible for auto-approval: %s",
event.pr_number,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f9d65bdb5c0>
mock_github_client = <MagicMock name='github_client' id='140313987003936'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:36:11.118499Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:36:11.119347Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "pr_author": "contributor", "reason": null}, "event": "PR #42 not eligible for auto-approval: None", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:36:11.119696Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:36:11.118499Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:36:11.119347Z'}
INFO stampbot.webhook_handler:webhook_handler.py:424 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'pr_author': 'contributor', 'reason': None}, 'event': 'PR #42 not eligible for auto-approval: None', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:36:11.119696Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 6, definition_name: _approval_needs_refresh
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -553,7 +553,7 @@
Returns:
True when a prior approval exists but no active approval applies to the current head.
"""
- if not reviews:
+ if reviews:
return False
return not self._has_current_active_approval(reviews, head_sha)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
....................F
=================================== FAILURES ===================================
_________ test_pr_labeled_unrelated_label_with_approval_label_ignored __________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f3753a643b0>
mock_github_client = <MagicMock name='github_client' id='139875597870016'>
@pytest.mark.asyncio
async def test_pr_labeled_unrelated_label_with_approval_label_ignored(
webhook_handler, mock_github_client
):
"""Test unrelated label changes do not re-approve just because an approval label exists."""
payload = load_fixture("pr_labeled_autoapprove")
payload["label"] = {"name": "bug"}
payload["pull_request"]["labels"].append({"name": "bug"})
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "ignored"
E AssertionError: assert 'success' == 'ignored'
E
E - ignored
E + success
tests/test_webhook_handler.py:496: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:51:30.038357Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:51:30.038893Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:51:30.039268Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:51:30.038357Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:51:30.038893Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:51:30.039268Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_unrelated_label_with_approval_label_ignored - AssertionError: assert 'success' == 'ignored'
- ignored
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 380 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 7, definition_name: _approval_needs_refresh
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -556,7 +556,7 @@
if not reviews:
return False
- return not self._has_current_active_approval(reviews, head_sha)
+ return self._has_current_active_approval(reviews, head_sha)
def _active_approvals_for_head(
self,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.....................F
=================================== FAILURES ===================================
_________ test_pr_labeled_unrelated_label_reapproves_dismissed_review __________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb7ae212270>
mock_github_client = <MagicMock name='github_client' id='140426878810640'>
@pytest.mark.asyncio
async def test_pr_labeled_unrelated_label_reapproves_dismissed_review(
webhook_handler, mock_github_client
):
"""Test unrelated label changes re-approve when prior Stampbot approval was dismissed."""
payload = load_fixture("pr_labeled_autoapprove")
payload["label"] = {"name": "bug"}
payload["pull_request"]["labels"].append({"name": "bug"})
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 123, "state": "DISMISSED", "commit_id": "abc123def456"}
]
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:516: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:45:25.244925Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:45:25.245454Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:45:25.244925Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:45:25.245454Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_unrelated_label_reapproves_dismissed_review - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 381 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 8, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return head_sha or not commit or commit == head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.....................................................................F
=================================== FAILURES ===================================
___________ test_active_approvals_for_head[reviews3-head-expected3] ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f29e07056d0>
reviews = [{'id': 1, 'state': 'APPROVED', 'commit_id': 'old'}, {'id': 2, 'state': 'APPROVED', 'commit_id': 'head'}]
head_sha = 'head', expected = [2]
@pytest.mark.parametrize(
("reviews", "head_sha", "expected"),
[
([], "head", []),
([{"id": 1, "state": "APPROVED", "commit_id": "head"}], "head", [1]),
(
[
{"id": 3, "state": "APPROVED", "commit_id": "head"},
{"id": 1, "state": "APPROVED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[1, 2, 3],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": "old"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": None},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
None,
[1, 2],
),
(
[
{"id": 1, "state": "DISMISSED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
],
)
def test_active_approvals_for_head(webhook_handler, reviews, head_sha, expected):
"""Test which approvals count as covering the head, oldest first."""
> assert webhook_handler._active_approvals_for_head(reviews, head_sha) == expected
E assert [1, 2] == [2]
E
E At index 0 diff: 1 != 2
E Left contains one more item: 2
E
E Full diff:
E [
E + 1,
E 2,
E ]
tests/test_webhook_handler.py:256: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_active_approvals_for_head[reviews3-head-expected3] - assert [1, 2] == [2]
At index 0 diff: 1 != 2
Left contains one more item: 2
Full diff:
[
+ 1,
2,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 357 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 9, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha or commit or commit == head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.....................................................................F
=================================== FAILURES ===================================
___________ test_active_approvals_for_head[reviews3-head-expected3] ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc1f46e16d0>
reviews = [{'id': 1, 'state': 'APPROVED', 'commit_id': 'old'}, {'id': 2, 'state': 'APPROVED', 'commit_id': 'head'}]
head_sha = 'head', expected = [2]
@pytest.mark.parametrize(
("reviews", "head_sha", "expected"),
[
([], "head", []),
([{"id": 1, "state": "APPROVED", "commit_id": "head"}], "head", [1]),
(
[
{"id": 3, "state": "APPROVED", "commit_id": "head"},
{"id": 1, "state": "APPROVED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[1, 2, 3],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": "old"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": None},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
None,
[1, 2],
),
(
[
{"id": 1, "state": "DISMISSED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
],
)
def test_active_approvals_for_head(webhook_handler, reviews, head_sha, expected):
"""Test which approvals count as covering the head, oldest first."""
> assert webhook_handler._active_approvals_for_head(reviews, head_sha) == expected
E assert [1, 2] == [2]
E
E At index 0 diff: 1 != 2
E Left contains one more item: 2
E
E Full diff:
E [
E + 1,
E 2,
E ]
tests/test_webhook_handler.py:256: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_active_approvals_for_head[reviews3-head-expected3] - assert [1, 2] == [2]
At index 0 diff: 1 != 2
Left contains one more item: 2
Full diff:
[
+ 1,
2,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 357 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 10, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -660,7 +660,7 @@
"chatops.commenter": commenter,
},
) as span:
- if not all([pr_number, repo_full_name, installation_id]):
+ if all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc0874f0200>
mock_github_client = <MagicMock name='github_client' id='140464873484848'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:53:56.239189Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in comment event", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:53:56.239338Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:53:56.239189Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:664 {'event': 'Missing required fields in comment event', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:53:56.239338Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 11, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -688,7 +688,7 @@
"message": "Invalid repository configuration",
}
- if not repo_config.chatops_enabled:
+ if repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1d2a8f0200>
mock_github_client = <MagicMock name='github_client' id='139763237564976'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:34:30.954373Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:34:30.955036Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:34:30.954373Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:34:30.955036Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 12, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -695,7 +695,7 @@
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
- if not command_match:
+ if command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7feff0cf0c20>
mock_github_client = <MagicMock name='github_client' id='140668506976816'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:04:26.003400Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:04:26.004028Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:04:26.003400Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:04:26.004028Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 13, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -735,7 +735,7 @@
commenter,
repo_config.chatops_required_permission,
)
- if not has_permission:
+ if has_permission:
chatops_commands_total.labels(command=command, status="forbidden").inc()
add_span_attributes(
span,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f4df4fdc200>
mock_github_client = <MagicMock name='github_client' id='139972792676912'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:46:05.101917Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:46:05.102504Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:46:05.101917Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:46:05.102504Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 14, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -952,7 +952,7 @@
"approval.trigger_type": trigger_type,
},
) as span:
- if not skip_existing_check:
+ if skip_existing_check:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f5e007f9940>
mock_github_client = <MagicMock name='github_client' id='140041705321008'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140041705318992'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:31:12.910978Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:31:12.911598Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:31:12.911878Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:31:12.912437Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:31:12.912729Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:31:12.910978Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:31:12.911598Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:31:12.911878Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:31:12.912437Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:31:12.912729Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 15, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1010,7 +1010,7 @@
status=status,
).inc()
- if not success:
+ if success:
errors_total.labels(error_type="approval_failed").inc()
else:
# Another replica may have approved the same head in the same........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f558aac48d0>
mock_github_client = <MagicMock name='github_client' id='140005373316128'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140005373316800'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:24:12.261949Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:24:12.262489Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:24:12.262756Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:24:12.261949Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:24:12.262489Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:24:12.262756Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 16, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1090,7 +1090,7 @@
add_span_attributes(span, {"dismissal.reviews_found": len(review_ids)})
- if not review_ids:
+ if review_ids:
logger.debug(
"No Stampbot approvals to dismiss on PR #%d",
pr_number,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7efe854a88d0>
mock_github_client = <MagicMock name='github_client' id='139631622229024'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='139631622229696'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:22:24.226968Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:22:24.227531Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:22:24.227791Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:22:24.226968Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:22:24.227531Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:22:24.227791Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 0, definition_name: head_sha
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -72,7 +72,7 @@
def head_sha(self) -> str | None:
"""Current head commit, when GitHub included it."""
sha = self.pr.get("head", {}).get("sha")
- return str(sha) if sha else None
+ return str(sha) if not sha else None
class WebhookHandler:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f4c666f88d0>
mock_github_client = <MagicMock name='github_client' id='139966111304736'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='139966111305408'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:24:04.677559Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:24:04.678141Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:24:04.678397Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:24:04.677559Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:24:04.678141Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:24:04.678397Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 1, definition_name: webhook_secret
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -93,7 +93,7 @@
Raises:
RuntimeError: If webhook secret is not configured.
"""
- if self._webhook_secret is None:
+ if not self._webhook_secret is None:
if not settings.webhook_secret:
raise RuntimeError(
"Webhook secret not configured. Visit /setup to create your GitHub App."........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f8ce71a7680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:118: in verify_signature
"sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/hmac.py:218: in new
return HMAC(key, msg, digestmod)
^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <hmac.HMAC object at 0x7f8ce6cfa700>, key = None
msg = b'{"test": "data"}', digestmod = <built-in function openssl_sha256>
def __init__(self, key, msg=None, digestmod=''):
"""Create a new HMAC object.
key: bytes or buffer, key for the keyed hash object.
msg: bytes or buffer, Initial input for the hash or None.
digestmod: A hash name suitable for hashlib.new(). *OR*
A hashlib constructor returning a new hash object. *OR*
A module supporting PEP 247.
Required as of 3.8, despite its position after the optional
msg argument. Passing it as a keyword argument is
recommended, though not required for legacy API reasons.
"""
if not isinstance(key, (bytes, bytearray)):
> raise TypeError(f"key: expected bytes or bytearray, "
f"but got {type(key).__name__!r}")
E TypeError: key: expected bytes or bytearray, but got 'NoneType'
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/hmac.py:71: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: key: expected bytes or bytearray, but got 'NoneType'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 2, definition_name: webhook_secret
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -94,7 +94,7 @@
RuntimeError: If webhook secret is not configured.
"""
if self._webhook_secret is None:
- if not settings.webhook_secret:
+ if not not settings.webhook_secret:
raise RuntimeError(
"Webhook secret not configured. Visit /setup to create your GitHub App."
)........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f5b9f96f680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:454: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:118: in verify_signature
"sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f5b9fedd010>
@property
def webhook_secret(self) -> bytes:
"""Get webhook secret, initializing if needed.
Returns:
Webhook secret as bytes.
Raises:
RuntimeError: If webhook secret is not configured.
"""
if self._webhook_secret is None:
if not not settings.webhook_secret:
> raise RuntimeError(
"Webhook secret not configured. Visit /setup to create your GitHub App."
)
E RuntimeError: Webhook secret not configured. Visit /setup to create your GitHub App.
stampbot/webhook_handler.py:98: RuntimeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - RuntimeError: Webhook secret not configured. Visit /setup to create your GitHub App.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 3, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -111,7 +111,7 @@
Returns:
True if signature is valid
"""
- if not signature:
+ if not not signature:
return False
expected_signature = (........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.F
=================================== FAILURES ===================================
__________________________ test_webhook_invalid_json ___________________________
test_client = <starlette.testclient.TestClient object at 0x7f23606c8270>
def test_webhook_invalid_json(test_client: TestClient):
"""Test webhook rejects invalid JSON payload."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
# Create a valid signature for invalid JSON
body = b"not valid json"
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 400
E assert 401 == 400
E + where 401 = <Response [401 Unauthorized]>.status_code
tests/test_main.py:330: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid webhook signature", "client_ip": "testclient", "logger": "stampbot.main", "level": "warning", "timestamp": "2026-09-04T21:14:23.864302Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 401 Unauthorized\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:14:23.865085Z"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.main:main.py:478 {'event': 'Invalid webhook signature', 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'warning', 'timestamp': '2026-09-04T21:14:23.864302Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 401 Unauthorized"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_invalid_json - assert 401 == 400
+ where 401 = <Response [401 Unauthorized]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 217 passed, 3 deselected, 1 warning in 3.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 4, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -149,7 +149,7 @@
"webhook.event_type": event_type,
"webhook.action": action,
}
- if delivery_id:
+ if not delivery_id:
span_attributes["github.delivery_id"] = delivery_id
with create_span("webhook.handle_event", span_attributes) as span:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
................F
=================================== FAILURES ===================================
____________________ test_handle_event_records_delivery_id _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f74c6494470>
@pytest.mark.asyncio
async def test_handle_event_records_delivery_id(webhook_handler):
"""Test the GitHub delivery GUID is attached to the event span."""
with patch("stampbot.webhook_handler.create_span") as mock_span:
mock_span.return_value.__enter__ = MagicMock(return_value=None)
mock_span.return_value.__exit__ = MagicMock(return_value=False)
await webhook_handler.handle_event("ping", {"zen": "x"}, delivery_id="abc-123")
attributes = mock_span.call_args_list[0].args[1]
> assert attributes["github.delivery_id"] == "abc-123"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E KeyError: 'github.delivery_id'
tests/test_webhook_handler.py:444: KeyError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:26:46.196537Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:26:46.196537Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_handle_event_records_delivery_id - KeyError: 'github.delivery_id'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 376 passed, 3 deselected, 1 warning in 4.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 5, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -160,7 +160,7 @@
)
# Route to appropriate handler
- if event_type == "pull_request":
+ if not event_type == "pull_request":
result = await self._handle_pull_request(payload)
elif event_type == "pull_request_review_comment":
result = await self._handle_pr_comment(payload)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7ff085ff19d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
assert response.status_code == 200
data = response.json()
> assert data["status"] == "ok"
E AssertionError: assert 'error' == 'ok'
E
E - ok
E + error
tests/test_main.py:362: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:59:18.839721Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in PR event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:59:18.840344Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T20:59:18.841166Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:59:18.839721Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:212 {'event': 'Missing required fields in PR event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:59:18.840344Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - AssertionError: assert 'error' == 'ok'
- ok
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.53s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 6, definition_name: handle_event
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -166,7 +166,7 @@
result = await self._handle_pr_comment(payload)
elif event_type == "issue_comment":
# Issue comments can be on PRs too
- if "pull_request" in payload.get("issue", {}):
+ if not "pull_request" in payload.get("issue", {}):
result = await self._handle_pr_comment(payload)
else:
result = {"status": "ignored", "message": "Not a PR comment"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fae8dbf16d0>
mock_github_client = <MagicMock name='github_client' id='140387672945200'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:41:19.869009Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:41:19.869009Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 7, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -208,7 +208,7 @@
"github.action": action or "unknown",
},
) as span:
- if not all([pr_number, repo_full_name, installation_id]):
+ if not not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f18e39db5c0>
mock_github_client = <MagicMock name='github_client' id='139744867563040'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:20:35.433386Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in PR event", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:20:35.433556Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:20:35.433386Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:212 {'event': 'Missing required fields in PR event', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:20:35.433556Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 8, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -214,7 +214,7 @@
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
- if action not in PULL_REQUEST_ACTIONS:
+ if not action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f23fedc35c0>
mock_github_client = <MagicMock name='github_client' id='139792569988640'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:48:37.944541Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:48:37.944541Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 9, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -229,7 +229,7 @@
owner_type,
)
- if repo_config.config_error:
+ if not repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6b3d0ef5c0>
mock_github_client = <MagicMock name='github_client' id='140098555999776'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:09:23.000132Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:09:23.000895Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": null}, "event": "Invalid Stampbot configuration for octocat/hello-world: None", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:09:23.001033Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:09:23.000132Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:09:23.000895Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': None}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: None', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:09:23.001033Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 10, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -244,7 +244,7 @@
},
)
- if action == "opened":
+ if not action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
...F
=================================== FAILURES ===================================
_____ test_missing_repo_config_with_invalid_service_defaults_fails_closed ______
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f819ed241d0>
mock_github_client = <MagicMock name='github_client' id='140194692475472'>
@pytest.mark.asyncio
async def test_missing_repo_config_with_invalid_service_defaults_fails_closed(
webhook_handler, mock_github_client
):
"""Test invalid service defaults block a PR when no repo config exists."""
payload = load_fixture("pr_opened_with_autoapprove_label")
service_defaults = MagicMock(spec=["approve_commands"])
service_defaults.approve_commands = "approve"
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
result = await webhook_handler.handle_event("pull_request", payload)
assert result == {"status": "error", "message": "Invalid repository configuration"}
> review_body = mock_github_client.create_pr_review_comment.call_args.args[3]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'args'
tests/test_webhook_handler.py:1292: AttributeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:48:47.939395Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.repo_policy", "level": "warning", "timestamp": "2026-09-04T20:48:47.939983Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "warning"}
{"extra": {"repo": "octocat/hello-world", "error": "Invalid service default configuration: approve_commands must be a list of strings"}, "event": "Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:48:47.940110Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:48:47.939395Z'}
WARNING stampbot.repo_policy:repo_policy.py:142 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid service default configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.repo_policy', 'level': 'warning', 'timestamp': '2026-09-04T20:48:47.939983Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': 'Invalid service default configuration: approve_commands must be a list of strings'}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: Invalid service default configuration: approve_commands must be a list of strings', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:48:47.940110Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_missing_repo_config_with_invalid_service_defaults_fails_closed - AttributeError: 'NoneType' object has no attribute 'args'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 435 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 11, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -275,7 +275,7 @@
)
decision, label = self._decide(event)
- if decision is Decision.APPROVE and label:
+ if not decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f27be5ef5c0>
mock_github_client = <MagicMock name='github_client' id='139808667317792'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:18:24.121142Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:18:24.121937Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:18:24.121142Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:18:24.121937Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 12, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -286,7 +286,7 @@
set_span_ok(span)
result = dict(NO_ACTION)
- if action == "opened":
+ if not action == "opened":
# This only produces an operator warning, so it runs after the
# review decision instead of delaying it.
await self._warn_missing_approval_labels(event)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......F
=================================== FAILURES ===================================
__________________ test_pr_opened_missing_label_logs_warning ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f0a1a99e510>
mock_github_client = <MagicMock name='github_client' id='139681367379312'>
@pytest.mark.asyncio
async def test_pr_opened_missing_label_logs_warning(webhook_handler, mock_github_client):
"""Test missing approval label triggers label check after the review decision."""
payload = load_fixture("pr_opened_with_autoapprove_label")
mock_github_client.repo_has_label.return_value = False
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
# The label on the pull request exists by definition; only "stamp" is checked.
> mock_github_client.repo_has_label.assert_called_once_with(12345, "octocat/hello-world", "stamp")
tests/test_webhook_handler.py:352:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.repo_has_label' id='139681367385024'>
args = (12345, 'octocat/hello-world', 'stamp'), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:28:11.413316Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:28:11.413870Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:28:11.414123Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:28:11.413316Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:28:11.413870Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:28:11.414123Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_missing_label_logs_warning - AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 367 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 13, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -303,7 +303,7 @@
The decision and the approval label it concerns, if any.
"""
config = event.repo_config
- if not config.auto_approve_on_label:
+ if not not config.auto_approve_on_label:
return Decision.NONE, None
if event.action == "unlabeled":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa9466e35c0>
mock_github_client = <MagicMock name='github_client' id='140365002274336'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:05:40.901356Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:05:40.902199Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:05:40.901356Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:05:40.902199Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 14, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -306,7 +306,7 @@
if not config.auto_approve_on_label:
return Decision.NONE, None
- if event.action == "unlabeled":
+ if not event.action == "unlabeled":
if event.event_label in config.approval_labels:
return Decision.DISMISS, event.event_label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc5f67c35c0>
mock_github_client = <MagicMock name='github_client' id='140488214197792'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:49:29.774675Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:49:29.775497Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:49:29.774675Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:49:29.775497Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 15, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -307,7 +307,7 @@
return Decision.NONE, None
if event.action == "unlabeled":
- if event.event_label in config.approval_labels:
+ if not event.event_label in config.approval_labels:
return Decision.DISMISS, event.event_label
return Decision.NONE, None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6035122990>
mock_github_client = <MagicMock name='github_client' id='140051026114752'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "error"
E AssertionError: assert 'ignored' == 'error'
E
E - error
E + ignored
tests/test_webhook_handler.py:210: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:55:41.972117Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:55:41.972693Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:55:41.972117Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:55:41.972693Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - AssertionError: assert 'ignored' == 'error'
- error
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 16, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -312,7 +312,7 @@
return Decision.NONE, None
label = self._find_approval_label(event.labels, config)
- if label is None:
+ if not label is None:
return Decision.NONE, None
if event.action in ("opened", "reopened"):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f43206475c0>
mock_github_client = <MagicMock name='github_client' id='139926278272544'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:11:11.865950Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:11:11.866722Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:11:11.865950Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:11:11.866722Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 3.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 17, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -315,7 +315,7 @@
if label is None:
return Decision.NONE, None
- if event.action in ("opened", "reopened"):
+ if not event.action in ("opened", "reopened"):
# GitHub also sends a labeled event for every label present when a
# pull request is created. Both events approve, because a missed
# approval costs more than a duplicate one; _approve_pr dismisses........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb2523eb5c0>
mock_github_client = <MagicMock name='github_client' id='140403854194208'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:32:48.558175Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:32:48.558967Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:32:48.558175Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:32:48.558967Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 3.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 18, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -322,7 +322,7 @@
# any duplicate afterwards.
return Decision.APPROVE, label
- if event.action == "labeled":
+ if not event.action == "labeled":
if event.event_label in config.approval_labels:
return Decision.APPROVE, label
return Decision.REFRESH, label........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1f508e5940>
mock_github_client = <MagicMock name='github_client' id='139772466082352'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
> assert [r["status"] for r in results] == ["success", "success"]
E AssertionError: assert ['success', 'ignored'] == ['success', 'success']
E
E At index 1 diff: 'ignored' != 'success'
E
E Full diff:
E [
E 'success',
E - 'success',
E + 'ignored',
E ]
tests/test_webhook_handler.py:116: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:14:15.105929Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:14:15.106518Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:14:15.106787Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:14:15.107423Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:14:15.105929Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:14:15.106518Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:14:15.106787Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:14:15.107423Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: assert ['success', 'ignored'] == ['success', 'success']
At index 1 diff: 'ignored' != 'success'
Full diff:
[
'success',
- 'success',
+ 'ignored',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 19, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -323,7 +323,7 @@
return Decision.APPROVE, label
if event.action == "labeled":
- if event.event_label in config.approval_labels:
+ if not event.event_label in config.approval_labels:
return Decision.APPROVE, label
return Decision.REFRESH, label
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fde49969940>
mock_github_client = <MagicMock name='github_client' id='140592688017968'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
> assert [r["status"] for r in results] == ["success", "success"]
E AssertionError: assert ['success', 'ignored'] == ['success', 'success']
E
E At index 1 diff: 'ignored' != 'success'
E
E Full diff:
E [
E 'success',
E - 'success',
E + 'ignored',
E ]
tests/test_webhook_handler.py:116: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:07:20.379068Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:07:20.379707Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:07:20.379998Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:07:20.380687Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:07:20.379068Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:07:20.379707Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:07:20.379998Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:07:20.380687Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: assert ['success', 'ignored'] == ['success', 'success']
At index 1 diff: 'ignored' != 'success'
Full diff:
[
'success',
- 'success',
+ 'ignored',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 20, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -327,7 +327,7 @@
return Decision.APPROVE, label
return Decision.REFRESH, label
- if event.action == "synchronize" and config.reapprove:
+ if not event.action == "synchronize" and config.reapprove:
return Decision.REFRESH, label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc2196a7a70>
mock_github_client = <MagicMock name='github_client' id='140471569151856'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:55:48.416574Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:55:48.416574Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 21, definition_name: _find_approval_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -343,7 +343,7 @@
Matching approval label, if present.
"""
for label in repo_config.approval_labels:
- if label in labels:
+ if not label in labels:
return label
return None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6a1b34b5c0>
mock_github_client = <MagicMock name='github_client' id='140093693605408'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
assert "approved" in result["message"].lower()
> mock_github_client.approve_pr.assert_called_once_with(
12345, # installation_id
"octocat/hello-world", # repo
42, # pr_number
"Auto-approved by Stampbot (label: autoapprove)",
)
tests/test_webhook_handler.py:79:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140093693602720'>
args = (12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')
kwargs = {}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: approve_pr(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')
E Actual: approve_pr(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: stamp)')
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:50:33.273196Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:50:33.274029Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "stamp"}, "event": "PR #42 has approval label: stamp", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:50:33.274336Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:50:33.273196Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:50:33.274029Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'stamp'}, 'event': 'PR #42 has approval label: stamp', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:50:33.274336Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: expected call not found.
Expected: approve_pr(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')
Actual: approve_pr(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: stamp)')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 22, definition_name: _refresh_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -366,7 +366,7 @@
event.repo_full_name,
event.pr_number,
)
- if reviews is None:
+ if not reviews is None:
# GitHub could not be read. Approving may create a duplicate that
# the cleanup removes; skipping could miss an approval for good.
logger.warning(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fcf4863e8a0>
mock_github_client = <MagicMock name='github_client' id='140528160634736'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:17:28.961021Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:17:28.961021Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 23, definition_name: _approve_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -404,7 +404,7 @@
pr_author = event.pr.get("user", {}).get("login", "")
author_team_slugs: list[str] | None = None
- if config.needs_team_check(pr_author) and event.owner_login:
+ if not config.needs_team_check(pr_author) and event.owner_login:
author_team_slugs = await run_in_threadpool(
github_client.get_user_team_slugs,
event.installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
..........................F
=================================== FAILURES ===================================
________________ test_pr_with_allowed_teams_triggers_team_check ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f875e00e1b0>
mock_github_client = <MagicMock name='github_client' id='140219372461360'>
@pytest.mark.asyncio
async def test_pr_with_allowed_teams_triggers_team_check(webhook_handler, mock_github_client):
"""Test PR with allowed_teams configured triggers team membership check."""
payload = load_fixture("pr_labeled_autoapprove")
# Config with allowed_teams - user not in allowed_users so needs team check
mock_github_client.get_repo_file.return_value = """
allowed_teams = ["acme/release-team"]
"""
# User is in the allowed team
mock_github_client.get_user_team_slugs.return_value = ["release-team"]
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:1714: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:47:13.603059Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "pr_author": "contributor", "reason": "PR author not a member of any allowed team"}, "event": "PR #42 not eligible for auto-approval: PR author not a member of any allowed team", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:47:13.603775Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:47:13.603059Z'}
INFO stampbot.webhook_handler:webhook_handler.py:424 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'pr_author': 'contributor', 'reason': 'PR author not a member of any allowed team'}, 'event': 'PR #42 not eligible for auto-approval: PR author not a member of any allowed team', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:47:13.603775Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_with_allowed_teams_triggers_team_check - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 458 passed, 3 deselected, 1 warning in 5.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 24, definition_name: _approve_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -420,7 +420,7 @@
pr_author,
author_team_slugs,
)
- if not is_eligible:
+ if not not is_eligible:
logger.info(
"PR #%d not eligible for auto-approval: %s",
event.pr_number,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fad16b6f5c0>
mock_github_client = <MagicMock name='github_client' id='140381381129760'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:10:38.408426Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:10:38.409196Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "pr_author": "contributor", "reason": null}, "event": "PR #42 not eligible for auto-approval: None", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:10:38.409520Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:10:38.408426Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:10:38.409196Z'}
INFO stampbot.webhook_handler:webhook_handler.py:424 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'pr_author': 'contributor', 'reason': None}, 'event': 'PR #42 not eligible for auto-approval: None', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:10:38.409520Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 3.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 25, definition_name: _approve_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -459,7 +459,7 @@
add_span_attributes(
span,
{
- "webhook.result": "approved" if success else "approval_failed",
+ "webhook.result": "approved" if not success else "approval_failed",
"webhook.trigger_label": label,
},
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 26, definition_name: _approve_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -466,7 +466,7 @@
set_span_ok(span)
return {
- "status": "success" if success else "error",
+ "status": "success" if not success else "error",
"message": f"PR approved via label: {label}" if success else "Failed to approve PR",
}
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f9d0cf5b5c0>
mock_github_client = <MagicMock name='github_client' id='140312497927712'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:08:50.842771Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:08:50.843529Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:08:50.843836Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:08:50.842771Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:08:50.843529Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:08:50.843836Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 27, definition_name: _approve_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -467,7 +467,7 @@
return {
"status": "success" if success else "error",
- "message": f"PR approved via label: {label}" if success else "Failed to approve PR",
+ "message": f"PR approved via label: {label}" if not success else "Failed to approve PR",
}
async def _dismiss_for_label(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f35246cb5c0>
mock_github_client = <MagicMock name='github_client' id='139866215446048'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
> assert "approved" in result["message"].lower()
E AssertionError: assert 'approved' in 'failed to approve pr'
E + where 'failed to approve pr' = <built-in method lower of str object at 0x7f3524b16b30>()
E + where <built-in method lower of str object at 0x7f3524b16b30> = 'Failed to approve PR'.lower
tests/test_webhook_handler.py:78: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:51.738005Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:20:51.738820Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:51.739123Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:51.738005Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:20:51.738820Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:51.739123Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'approved' in 'failed to approve pr'
+ where 'failed to approve pr' = <built-in method lower of str object at 0x7f3524b16b30>()
+ where <built-in method lower of str object at 0x7f3524b16b30> = 'Failed to approve PR'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 3.96s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 28, definition_name: _dismiss_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -501,7 +501,7 @@
add_span_attributes(
span,
{
- "webhook.result": "dismissed" if success else "dismiss_failed",
+ "webhook.result": "dismissed" if not success else "dismiss_failed",
"webhook.removed_label": label,
},
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 29, definition_name: _dismiss_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -508,7 +508,7 @@
set_span_ok(span)
return {
- "status": "success" if success else "error",
+ "status": "success" if not success else "error",
"message": "Approvals dismissed" if success else "Failed to dismiss approvals",
}
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f013daf8aa0>
mock_github_client = <MagicMock name='github_client' id='139643294660800'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "error"
E AssertionError: assert 'success' == 'error'
E
E - error
E + success
tests/test_webhook_handler.py:210: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:01:01.241185Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:01:01.241795Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:01:01.241941Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "Stampbot reviews could not be listed"}, "event": "Error dismissing approvals: Stampbot reviews could not be listed", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T23:01:01.242206Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:01:01.241185Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:01:01.241795Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:01:01.241941Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': 'Stampbot reviews could not be listed'}, 'event': 'Error dismissing approvals: Stampbot reviews could not be listed', 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T23:01:01.242206Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - AssertionError: assert 'success' == 'error'
- error
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 30, definition_name: _dismiss_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -509,7 +509,7 @@
return {
"status": "success" if success else "error",
- "message": "Approvals dismissed" if success else "Failed to dismiss approvals",
+ "message": "Approvals dismissed" if not success else "Failed to dismiss approvals",
}
async def _warn_missing_approval_labels(self, event: PullRequestEvent) -> None:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................F
=================================== FAILURES ===================================
________________________ test_pr_unlabeled_autoapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb969df5cd0>
mock_github_client = <MagicMock name='github_client' id='140434321070400'>
@pytest.mark.asyncio
async def test_pr_unlabeled_autoapprove(webhook_handler, mock_github_client):
"""Test removing autoapprove label dismisses approvals."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 222, "state": "APPROVED", "commit_id": "abc123def456"},
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
> assert "dismissed" in result["message"].lower()
E AssertionError: assert 'dismissed' in 'failed to dismiss approvals'
E + where 'failed to dismiss approvals' = <built-in method lower of str object at 0x7fb96a8ed020>()
E + where <built-in method lower of str object at 0x7fb96a8ed020> = 'Failed to dismiss approvals'.lower
tests/test_webhook_handler.py:675: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:15:37.790101Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:15:37.790625Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:15:37.790760Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:15:37.790101Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:15:37.790625Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:15:37.790760Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_autoapprove - AssertionError: assert 'dismissed' in 'failed to dismiss approvals'
+ where 'failed to dismiss approvals' = <built-in method lower of str object at 0x7fb96a8ed020>()
+ where <built-in method lower of str object at 0x7fb96a8ed020> = 'Failed to dismiss approvals'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 400 passed, 3 deselected, 1 warning in 4.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 31, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -523,7 +523,7 @@
"""
present = event.labels
for label in event.repo_config.approval_labels:
- if label in present:
+ if not label in present:
continue
label_exists = await run_in_threadpool(
github_client.repo_has_label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......F
=================================== FAILURES ===================================
__________________ test_pr_opened_missing_label_logs_warning ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbc4c701fd0>
mock_github_client = <MagicMock name='github_client' id='140446707824160'>
@pytest.mark.asyncio
async def test_pr_opened_missing_label_logs_warning(webhook_handler, mock_github_client):
"""Test missing approval label triggers label check after the review decision."""
payload = load_fixture("pr_opened_with_autoapprove_label")
mock_github_client.repo_has_label.return_value = False
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
# The label on the pull request exists by definition; only "stamp" is checked.
> mock_github_client.repo_has_label.assert_called_once_with(12345, "octocat/hello-world", "stamp")
tests/test_webhook_handler.py:352:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.repo_has_label' id='140446707813744'>
args = (12345, 'octocat/hello-world', 'stamp'), kwargs = {}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: repo_has_label(12345, 'octocat/hello-world', 'stamp')
E Actual: repo_has_label(12345, 'octocat/hello-world', 'autoapprove')
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:46:31.573821Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:46:31.574347Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:46:31.574618Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "label": "autoapprove"}, "event": "Approval label autoapprove not found in octocat/hello-world", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T22:46:31.575189Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:46:31.573821Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:46:31.574347Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:46:31.574618Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:535 {'extra': {'repo': 'octocat/hello-world', 'label': 'autoapprove'}, 'event': 'Approval label autoapprove not found in octocat/hello-world', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T22:46:31.575189Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_missing_label_logs_warning - AssertionError: expected call not found.
Expected: repo_has_label(12345, 'octocat/hello-world', 'stamp')
Actual: repo_has_label(12345, 'octocat/hello-world', 'autoapprove')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 367 passed, 3 deselected, 1 warning in 4.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 32, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -531,7 +531,7 @@
event.repo_full_name,
label,
)
- if label_exists is False:
+ if not label_exists is False:
logger.warning(
"Approval label %s not found in %s",
label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 33, definition_name: _approval_needs_refresh
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -553,7 +553,7 @@
Returns:
True when a prior approval exists but no active approval applies to the current head.
"""
- if not reviews:
+ if not not reviews:
return False
return not self._has_current_active_approval(reviews, head_sha)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
....................F
=================================== FAILURES ===================================
_________ test_pr_labeled_unrelated_label_with_approval_label_ignored __________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f51c52c3d70>
mock_github_client = <MagicMock name='github_client' id='139989177279424'>
@pytest.mark.asyncio
async def test_pr_labeled_unrelated_label_with_approval_label_ignored(
webhook_handler, mock_github_client
):
"""Test unrelated label changes do not re-approve just because an approval label exists."""
payload = load_fixture("pr_labeled_autoapprove")
payload["label"] = {"name": "bug"}
payload["pull_request"]["labels"].append({"name": "bug"})
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "ignored"
E AssertionError: assert 'success' == 'ignored'
E
E - ignored
E + success
tests/test_webhook_handler.py:496: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:18:39.327496Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:18:39.328040Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:18:39.328413Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:18:39.327496Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:18:39.328040Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:18:39.328413Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_unrelated_label_with_approval_label_ignored - AssertionError: assert 'success' == 'ignored'
- ignored
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 380 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 34, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -585,7 +585,7 @@
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
- if proven_only:
+ if not proven_only:
return bool(head_sha) and commit == head_sha
return not head_sha or not commit or commit == head_sha
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
......................................................................F
=================================== FAILURES ===================================
___________ test_active_approvals_for_head[reviews4-None-expected4] ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbbcd36d6d0>
reviews = [{'id': 1, 'state': 'APPROVED', 'commit_id': None}, {'id': 2, 'state': 'APPROVED', 'commit_id': 'head'}]
head_sha = None, expected = [1, 2]
@pytest.mark.parametrize(
("reviews", "head_sha", "expected"),
[
([], "head", []),
([{"id": 1, "state": "APPROVED", "commit_id": "head"}], "head", [1]),
(
[
{"id": 3, "state": "APPROVED", "commit_id": "head"},
{"id": 1, "state": "APPROVED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[1, 2, 3],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": "old"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": None},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
None,
[1, 2],
),
(
[
{"id": 1, "state": "DISMISSED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
],
)
def test_active_approvals_for_head(webhook_handler, reviews, head_sha, expected):
"""Test which approvals count as covering the head, oldest first."""
> assert webhook_handler._active_approvals_for_head(reviews, head_sha) == expected
E assert [] == [1, 2]
E
E Right contains 2 more items, first extra item: 1
E
E Full diff:
E + []
E - [
E - 1,
E - 2,
E - ]
tests/test_webhook_handler.py:256: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_active_approvals_for_head[reviews4-None-expected4] - assert [] == [1, 2]
Right contains 2 more items, first extra item: 1
Full diff:
+ []
- [
- 1,
- 2,
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 358 passed, 3 deselected, 1 warning in 4.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 35, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -624,7 +624,7 @@
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
- if len(comment_body) > MAX_COMMENT_LENGTH:
+ if not len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f63ee05ec30>
mock_github_client = <MagicMock name='github_client' id='140067165942320'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:09:34.371788Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:09:34.371788Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 36, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -630,7 +630,7 @@
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
- if "@stampbot" not in comment_body:
+ if not "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7efc3fbfab10>
mock_github_client = <MagicMock name='github_client' id='139621856981552'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:37:06.554182Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:37:06.554182Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 37, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -634,7 +634,7 @@
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
- if "pull_request" in payload.get("issue", {}):
+ if not "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7faf2b7faba0>
mock_github_client = <MagicMock name='github_client' id='140390316782128'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:34:37.723416Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:34:37.723416Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.26s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 38, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -660,7 +660,7 @@
"chatops.commenter": commenter,
},
) as span:
- if not all([pr_number, repo_full_name, installation_id]):
+ if not not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc1848dc200>
mock_github_client = <MagicMock name='github_client' id='140469122478640'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:02:16.160219Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "Missing required fields in comment event", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:02:16.160378Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:02:16.160219Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:664 {'event': 'Missing required fields in comment event', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:02:16.160378Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.25s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 39, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -674,7 +674,7 @@
owner_type,
)
- if repo_config.config_error:
+ if not repo_config.config_error:
add_span_attributes(
span,
{........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe6426f15b0>
mock_github_client = <MagicMock name='github_client' id='140626928251440'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:17:28.143773Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:17:28.144341Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:17:28.143773Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:17:28.144341Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 40, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -688,7 +688,7 @@
"message": "Invalid repository configuration",
}
- if not repo_config.chatops_enabled:
+ if not not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f61292f1640>
mock_github_client = <MagicMock name='github_client' id='140055272321584'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:58:18.392788Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:58:18.393449Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:58:18.392788Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:58:18.393449Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 41, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -695,7 +695,7 @@
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
- if not command_match:
+ if not not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb2cf2eea80>
mock_github_client = <MagicMock name='github_client' id='140405947773488'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:39:32.310020Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:39:32.310625Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:39:32.310020Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:39:32.310625Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 42, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -704,7 +704,7 @@
command = command_match.group(1).lower()
add_span_attributes(span, {"chatops.command": command})
- if command == "help":
+ if not command == "help":
success = await self._post_help(
installation_id,
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..................................................F
=================================== FAILURES ===================================
__________________________ test_issue_comment_approve __________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f11cfaa2810>
mock_github_client = <MagicMock name='github_client' id='139714475282784'>
@pytest.mark.asyncio
async def test_issue_comment_approve(webhook_handler, mock_github_client):
"""Test @stampbot approve command."""
payload = load_fixture("issue_comment_approve")
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> assert "approved" in result["message"].lower()
E AssertionError: assert 'approved' in 'help message posted'
E + where 'help message posted' = <built-in method lower of str object at 0x7f11d4408030>()
E + where <built-in method lower of str object at 0x7f11d4408030> = 'Help message posted'.lower
tests/test_webhook_handler.py:830: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:59:00.420152Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:59:00.421540Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:59:00.420152Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:59:00.421540Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_approve - AssertionError: assert 'approved' in 'help message posted'
+ where 'help message posted' = <built-in method lower of str object at 0x7f11d4408030>()
+ where <built-in method lower of str object at 0x7f11d4408030> = 'Help message posted'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 410 passed, 3 deselected, 1 warning in 4.60s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 43, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -714,7 +714,7 @@
chatops_commands_total.labels(
command="help",
- status="success" if success else "failure",
+ status="success" if not success else "failure",
).inc()
add_span_attributes(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 44, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -718,7 +718,7 @@
).inc()
add_span_attributes(
- span, {"chatops.result": "help_posted" if success else "help_failed"}
+ span, {"chatops.result": "help_posted" if not success else "help_failed"}
)
set_span_ok(span)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 45, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -723,7 +723,7 @@
set_span_ok(span)
return {
- "status": "success" if success else "error",
+ "status": "success" if not success else "error",
"message": "Help message posted" if success else "Failed to post help message",
}
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...........................................................F
=================================== FAILURES ===================================
_______________________ test_issue_comment_help_command ________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa9c534f1d0>
mock_github_client = <MagicMock name='github_client' id='140367137309424'>
@pytest.mark.asyncio
async def test_issue_comment_help_command(webhook_handler, mock_github_client):
"""Test @stampbot help posts contextual help."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:974: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:36:05.111340Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:36:05.111941Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:36:05.111340Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:36:05.111941Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_command - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 419 passed, 3 deselected, 1 warning in 4.65s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 46, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -724,7 +724,7 @@
return {
"status": "success" if success else "error",
- "message": "Help message posted" if success else "Failed to post help message",
+ "message": "Help message posted" if not success else "Failed to post help message",
}
if command in repo_config.approve_commands + repo_config.unapprove_commands:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.31s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 47, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -727,7 +727,7 @@
"message": "Help message posted" if success else "Failed to post help message",
}
- if command in repo_config.approve_commands + repo_config.unapprove_commands:
+ if not command in repo_config.approve_commands + repo_config.unapprove_commands:
has_permission = await run_in_threadpool(
github_client.user_has_permission,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..................................................F
=================================== FAILURES ===================================
__________________________ test_issue_comment_approve __________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f36a91d3dd0>
mock_github_client = <MagicMock name='github_client' id='139872739146928'>
@pytest.mark.asyncio
async def test_issue_comment_approve(webhook_handler, mock_github_client):
"""Test @stampbot approve command."""
payload = load_fixture("issue_comment_approve")
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
assert "approved" in result["message"].lower()
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.user_has_permission.assert_called_once()
tests/test_webhook_handler.py:832:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.user_has_permission' id='139872738370496'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'user_has_permission' to have been called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:16:28.165844Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:16:28.166401Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:16:28.165844Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:16:28.166401Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_approve - AssertionError: Expected 'user_has_permission' to have been called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 410 passed, 3 deselected, 1 warning in 4.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 48, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -735,7 +735,7 @@
commenter,
repo_config.chatops_required_permission,
)
- if not has_permission:
+ if not not has_permission:
chatops_commands_total.labels(command=command, status="forbidden").inc()
add_span_attributes(
span,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f46f06e56d0>
mock_github_client = <MagicMock name='github_client' id='139942583398960'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:43:22.713632Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:43:22.714277Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:43:22.713632Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:43:22.714277Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 49, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -753,7 +753,7 @@
}
# Handle approve commands
- if command in repo_config.approve_commands:
+ if not command in repo_config.approve_commands:
head_sha = await run_in_threadpool(
github_client.get_pr_head_sha,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fcdf0cf0200>
mock_github_client = <MagicMock name='github_client' id='140522479039024'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:02:52.470406Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:02:52.471090Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:02:52.470406Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:02:52.471090Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.28s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 50, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -771,7 +771,7 @@
chatops_commands_total.labels(
command="approve",
- status="success" if success else "failure",
+ status="success" if not success else "failure",
).inc()
add_span_attributes(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.14s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 51, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -775,7 +775,7 @@
).inc()
add_span_attributes(
- span, {"chatops.result": "approved" if success else "approval_failed"}
+ span, {"chatops.result": "approved" if not success else "approval_failed"}
)
set_span_ok(span)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 52, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -780,7 +780,7 @@
set_span_ok(span)
return {
- "status": "success" if success else "error",
+ "status": "success" if not success else "error",
"message": "PR approved" if success else "Failed to approve PR",
}
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fdc1eef0200>
mock_github_client = <MagicMock name='github_client' id='140583383167536'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T23:03:41.386994Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T23:03:41.387626Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T23:03:41.386994Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T23:03:41.387626Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 53, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -781,7 +781,7 @@
return {
"status": "success" if success else "error",
- "message": "PR approved" if success else "Failed to approve PR",
+ "message": "PR approved" if not success else "Failed to approve PR",
}
# Handle unapprove commands........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..................................................F
=================================== FAILURES ===================================
__________________________ test_issue_comment_approve __________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6b9d90a2d0>
mock_github_client = <MagicMock name='github_client' id='140100183145648'>
@pytest.mark.asyncio
async def test_issue_comment_approve(webhook_handler, mock_github_client):
"""Test @stampbot approve command."""
payload = load_fixture("issue_comment_approve")
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> assert "approved" in result["message"].lower()
E AssertionError: assert 'approved' in 'failed to approve pr'
E + where 'failed to approve pr' = <built-in method lower of str object at 0x7f6b9e315db0>()
E + where <built-in method lower of str object at 0x7f6b9e315db0> = 'Failed to approve PR'.lower
tests/test_webhook_handler.py:830: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:47:06.137895Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:47:06.138443Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:47:06.137895Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:47:06.138443Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_approve - AssertionError: assert 'approved' in 'failed to approve pr'
+ where 'failed to approve pr' = <built-in method lower of str object at 0x7f6b9e315db0>()
+ where <built-in method lower of str object at 0x7f6b9e315db0> = 'Failed to approve PR'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 410 passed, 3 deselected, 1 warning in 4.59s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 54, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -796,7 +796,7 @@
chatops_commands_total.labels(
command="unapprove",
- status="success" if success else "failure",
+ status="success" if not success else "failure",
).inc()
add_span_attributes(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 55, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -800,7 +800,7 @@
).inc()
add_span_attributes(
- span, {"chatops.result": "unapproved" if success else "unapprove_failed"}
+ span, {"chatops.result": "unapproved" if not success else "unapprove_failed"}
)
set_span_ok(span)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 56, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -805,7 +805,7 @@
set_span_ok(span)
return {
- "status": "success" if success else "error",
+ "status": "success" if not success else "error",
"message": "Approvals dismissed" if success else "Failed to dismiss approvals",
}
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......................................................F
=================================== FAILURES ===================================
_________________________ test_issue_comment_unapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1556d93d70>
mock_github_client = <MagicMock name='github_client' id='139729625058672'>
@pytest.mark.asyncio
async def test_issue_comment_unapprove(webhook_handler, mock_github_client):
"""Test @stampbot unapprove command."""
payload = load_fixture("issue_comment_unapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"}
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:921: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:57:14.795525Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:57:14.796059Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:57:14.795525Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:57:14.796059Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_unapprove - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 415 passed, 3 deselected, 1 warning in 4.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 57, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -806,7 +806,7 @@
return {
"status": "success" if success else "error",
- "message": "Approvals dismissed" if success else "Failed to dismiss approvals",
+ "message": "Approvals dismissed" if not success else "Failed to dismiss approvals",
}
chatops_commands_total.labels(command="unknown", status="ignored").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......................................................F
=================================== FAILURES ===================================
_________________________ test_issue_comment_unapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f3b6d2b05f0>
mock_github_client = <MagicMock name='github_client' id='139893216513392'>
@pytest.mark.asyncio
async def test_issue_comment_unapprove(webhook_handler, mock_github_client):
"""Test @stampbot unapprove command."""
payload = load_fixture("issue_comment_unapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"}
]
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> assert "dismissed" in result["message"].lower()
E AssertionError: assert 'dismissed' in 'failed to dismiss approvals'
E + where 'failed to dismiss approvals' = <built-in method lower of str object at 0x7f3b6dc21160>()
E + where <built-in method lower of str object at 0x7f3b6dc21160> = 'Failed to dismiss approvals'.lower
tests/test_webhook_handler.py:922: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:48:59.877649Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:48:59.878171Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:48:59.877649Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:48:59.878171Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_unapprove - AssertionError: assert 'dismissed' in 'failed to dismiss approvals'
+ where 'failed to dismiss approvals' = <built-in method lower of str object at 0x7f3b6dc21160>()
+ where <built-in method lower of str object at 0x7f3b6dc21160> = 'Failed to dismiss approvals'.lower
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 415 passed, 3 deselected, 1 warning in 4.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 58, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -854,7 +854,7 @@
]
)
- if repo_config.auto_approve_on_label:
+ if not repo_config.auto_approve_on_label:
approval_labels = ", ".join(f"`{label}`" for label in repo_config.approval_labels)
lines.append(f"Adding any of these labels can trigger approval: {approval_labels}")
else:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...........................................................F
=================================== FAILURES ===================================
_______________________ test_issue_comment_help_command ________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb5bac17d70>
mock_github_client = <MagicMock name='github_client' id='140418498347760'>
@pytest.mark.asyncio
async def test_issue_comment_help_command(webhook_handler, mock_github_client):
"""Test @stampbot help posts contextual help."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
assert "help" in result["message"].lower()
mock_github_client.user_has_permission.assert_not_called()
mock_github_client.create_issue_comment.assert_called_once()
comment_args = mock_github_client.create_issue_comment.call_args[0]
assert comment_args[:3] == (12345, "octocat/hello-world", 42)
assert "@stampbot approve" in comment_args[3]
assert "@stampbot unapprove" in comment_args[3]
> assert "`autoapprove`" in comment_args[3]
E AssertionError: assert '`autoapprove`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....Help**: `@stampbot help`\n\n### Label-Based Auto-Approval\n\nLabel-based auto-approval is disabled in this repository.'
tests/test_webhook_handler.py:982: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:21:22.237886Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:21:22.238414Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:21:22.237886Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:21:22.238414Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_command - AssertionError: assert '`autoapprove`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....Help**: `@stampbot help`\n\n### Label-Based Auto-Approval\n\nLabel-based auto-approval is disabled in this repository.'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 419 passed, 3 deselected, 1 warning in 4.49s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 59, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -861,7 +861,7 @@
lines.append("Label-based auto-approval is disabled in this repository.")
filters = []
- if repo_config.required_labels:
+ if not repo_config.required_labels:
filters.append(
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f71e69d1130>
mock_github_client = <MagicMock name='github_client' id='140127172760576'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
help_text = mock_github_client.create_issue_comment.call_args[0][3]
assert "@stampbot approve-it" in help_text
assert "@stampbot hold-it" in help_text
assert "`ship-it`" in help_text
> assert "`dependencies`" in help_text
E AssertionError: assert '`dependencies`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....ilters apply:\n- required title patterns: `^chore:`\n- allowed users: `renovate[bot]`\n- allowed teams: `release-team`'
tests/test_webhook_handler.py:1007: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:37:22.894538Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:37:22.894538Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - AssertionError: assert '`dependencies`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....ilters apply:\n- required title patterns: `^chore:`\n- allowed users: `renovate[bot]`\n- allowed teams: `release-team`'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 60, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -866,7 +866,7 @@
"required labels: "
+ ", ".join(f"`{label}`" for label in repo_config.required_labels)
)
- if repo_config.required_title_patterns:
+ if not repo_config.required_title_patterns:
filters.append(
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f52cf83e8d0>
mock_github_client = <MagicMock name='github_client' id='139993644700672'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
help_text = mock_github_client.create_issue_comment.call_args[0][3]
assert "@stampbot approve-it" in help_text
assert "@stampbot hold-it" in help_text
assert "`ship-it`" in help_text
assert "`dependencies`" in help_text
> assert "`^chore:`" in help_text
E AssertionError: assert '`^chore:`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....l filters apply:\n- required labels: `dependencies`\n- allowed users: `renovate[bot]`\n- allowed teams: `release-team`'
tests/test_webhook_handler.py:1008: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:14:58.552241Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:14:58.552241Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - AssertionError: assert '`^chore:`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....l filters apply:\n- required labels: `dependencies`\n- allowed users: `renovate[bot]`\n- allowed teams: `release-team`'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 61, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -871,7 +871,7 @@
"required title patterns: "
+ ", ".join(f"`{pattern}`" for pattern in repo_config.required_title_patterns)
)
- if repo_config.allowed_users:
+ if not repo_config.allowed_users:
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2b93bdd190>
mock_github_client = <MagicMock name='github_client' id='139825134703616'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
help_text = mock_github_client.create_issue_comment.call_args[0][3]
assert "@stampbot approve-it" in help_text
assert "@stampbot hold-it" in help_text
assert "`ship-it`" in help_text
assert "`dependencies`" in help_text
assert "`^chore:`" in help_text
> assert "`renovate[bot]`" in help_text
E AssertionError: assert '`renovate[bot]`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....lters apply:\n- required labels: `dependencies`\n- required title patterns: `^chore:`\n- allowed teams: `release-team`'
tests/test_webhook_handler.py:1009: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:09:46.542999Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:09:46.542999Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - AssertionError: assert '`renovate[bot]`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....lters apply:\n- required labels: `dependencies`\n- required title patterns: `^chore:`\n- allowed teams: `release-team`'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.51s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 62, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -875,7 +875,7 @@
filters.append(
"allowed users: " + ", ".join(f"`{user}`" for user in repo_config.allowed_users)
)
- if repo_config.allowed_teams:
+ if not repo_config.allowed_teams:
filters.append(
"allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fed5a348350>
mock_github_client = <MagicMock name='github_client' id='140657394422784'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
help_text = mock_github_client.create_issue_comment.call_args[0][3]
assert "@stampbot approve-it" in help_text
assert "@stampbot hold-it" in help_text
assert "`ship-it`" in help_text
assert "`dependencies`" in help_text
assert "`^chore:`" in help_text
assert "`renovate[bot]`" in help_text
> assert "`release-team`" in help_text
E AssertionError: assert '`release-team`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....ters apply:\n- required labels: `dependencies`\n- required title patterns: `^chore:`\n- allowed users: `renovate[bot]`'
tests/test_webhook_handler.py:1010: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:18:17.901231Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:18:17.901231Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - AssertionError: assert '`release-team`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....ters apply:\n- required labels: `dependencies`\n- required title patterns: `^chore:`\n- allowed users: `renovate[bot]`'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.53s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 63, definition_name: _format_help_message
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -880,7 +880,7 @@
"allowed teams: " + ", ".join(f"`{team}`" for team in repo_config.allowed_teams)
)
- if filters:
+ if not filters:
lines.extend(["", "Additional approval filters apply:"])
lines.extend(f"- {filter_text}" for filter_text in filters)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............................................................F
=================================== FAILURES ===================================
_______________ test_issue_comment_help_uses_custom_repo_config ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f0937606450>
mock_github_client = <MagicMock name='github_client' id='139677560132608'>
@pytest.mark.asyncio
async def test_issue_comment_help_uses_custom_repo_config(webhook_handler, mock_github_client):
"""Test help reflects custom repo commands, labels, and filters."""
payload = load_fixture("issue_comment_approve")
payload["comment"]["body"] = "@stampbot help"
mock_github_client.get_repo_file.return_value = """
approval_labels = ["ship-it"]
approve_commands = ["approve-it"]
unapprove_commands = ["hold-it"]
required_labels = ["dependencies"]
required_title_patterns = ["^chore:"]
allowed_users = ["renovate[bot]"]
allowed_teams = ["release-team"]
"""
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
help_text = mock_github_client.create_issue_comment.call_args[0][3]
assert "@stampbot approve-it" in help_text
assert "@stampbot hold-it" in help_text
assert "`ship-it`" in help_text
> assert "`dependencies`" in help_text
E AssertionError: assert '`dependencies`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....elp**: `@stampbot help`\n\n### Label-Based Auto-Approval\n\nAdding any of these labels can trigger approval: `ship-it`'
tests/test_webhook_handler.py:1007: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:09:05.650589Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:09:05.650589Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_help_uses_custom_repo_config - AssertionError: assert '`dependencies`' in '## Stampbot Help\n\n### ChatOps Commands\n\nApprove and unapprove commands require **maintain** permission or higher....elp**: `@stampbot help`\n\n### Label-Based Auto-Approval\n\nAdding any of these labels can trigger approval: `ship-it`'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 420 passed, 3 deselected, 1 warning in 4.51s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 64, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -952,7 +952,7 @@
"approval.trigger_type": trigger_type,
},
) as span:
- if not skip_existing_check:
+ if not not skip_existing_check:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fd18fdd5940>
mock_github_client = <MagicMock name='github_client' id='140538031452720'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140538031451040'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:34.773433Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:29:34.774054Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:34.774325Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:34.774895Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:34.775152Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:34.773433Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:29:34.774054Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:34.774325Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:34.774895Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:34.775152Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 65, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -959,7 +959,7 @@
repo_full_name,
pr_number,
)
- if reviews is None:
+ if not reviews is None:
# Unknown state. A duplicate is recoverable; a missed
# approval is not, so approve and let the cleanup sort it out.
logger.warning(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbd27fe5940>
mock_github_client = <MagicMock name='github_client' id='140450391110192'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140450391108176'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:38:28.520050Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:38:28.520671Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:38:28.520940Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42}, "event": "Could not read reviews for PR #42 in octocat/hello-world; approving without a check", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:38:28.521182Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:38:28.521723Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:38:28.521976Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42}, "event": "Could not read reviews for PR #42 in octocat/hello-world; approving without a check", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:38:28.522195Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:38:28.520050Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:38:28.520671Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:38:28.520940Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:965 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42}, 'event': 'Could not read reviews for PR #42 in octocat/hello-world; approving without a check', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:38:28.521182Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:38:28.521723Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:38:28.521976Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:965 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42}, 'event': 'Could not read reviews for PR #42 in octocat/hello-world; approving without a check', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:38:28.522195Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 66, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -970,7 +970,7 @@
)
reviews = []
existing_approvals = self._active_approvals_for_head(reviews, head_sha)
- if existing_approvals:
+ if not existing_approvals:
logger.info(
"PR #%d in %s already has active approval for current head, skipping",
pr_number,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2b542035c0>
mock_github_client = <MagicMock name='github_client' id='139823997941280'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
assert "approved" in result["message"].lower()
> mock_github_client.approve_pr.assert_called_once_with(
12345, # installation_id
"octocat/hello-world", # repo
42, # pr_number
"Auto-approved by Stampbot (label: autoapprove)",
)
tests/test_webhook_handler.py:79:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='139823997938256'>
args = (12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:02:23.818028Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:02:23.818893Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:02:23.819237Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "existing_review_ids": []}, "event": "PR #42 in octocat/hello-world already has active approval for current head, skipping", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:02:23.819529Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:02:23.818028Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:02:23.818893Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:02:23.819237Z'}
INFO stampbot.webhook_handler:webhook_handler.py:974 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'existing_review_ids': []}, 'event': 'PR #42 in octocat/hello-world already has active approval for current head, skipping', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:02:23.819529Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: Expected 'approve_pr' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 67, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1004,7 +1004,7 @@
duration = time.time() - start_time
pr_approval_duration_seconds.observe(duration)
- status = "success" if success else "failure"
+ status = "success" if not success else "failure"
pr_approvals_total.labels(
trigger_type=trigger_type,
status=status,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 68, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1010,7 +1010,7 @@
status=status,
).inc()
- if not success:
+ if not not success:
errors_total.labels(error_type="approval_failed").inc()
else:
# Another replica may have approved the same head in the same........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc782a748d0>
mock_github_client = <MagicMock name='github_client' id='140494866369568'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140494866370240'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:03:59.899991Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:03:59.900568Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:03:59.900831Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:03:59.899991Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:03:59.900568Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:03:59.900831Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 69, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1075,7 +1075,7 @@
repo_full_name,
pr_number,
)
- if reviews is None:
+ if not reviews is None:
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f2816391bf0>
mock_github_client = <MagicMock name='github_client' id='139810145700896'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='139810145701568'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:27:55.501627Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:27:55.502177Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:27:55.502428Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "Stampbot reviews could not be listed"}, "event": "Error dismissing approvals: Stampbot reviews could not be listed", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T22:27:55.502957Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:27:55.501627Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:27:55.502177Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:27:55.502428Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': 'Stampbot reviews could not be listed'}, 'event': 'Error dismissing approvals: Stampbot reviews could not be listed', 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T22:27:55.502957Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 70, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1079,7 +1079,7 @@
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")
- if keep_oldest:
+ if not keep_oldest:
# Duplicate cleanup only touches approvals proven to be of
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6ead531940>
mock_github_client = <MagicMock name='github_client' id='140113326273072'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_not_called()
tests/test_webhook_handler.py:118:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140113326275760'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 777, 'Duplicate Stampbot approval')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:14:45.118150Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:14:45.118726Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:14:45.118997Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:14:45.119784Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:14:45.120045Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "existing_review_ids": [777]}, "event": "PR #42 in octocat/hello-world already has active approval for current head, skipping", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:14:45.120275Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:14:45.118150Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:14:45.118726Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:14:45.118997Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:14:45.119784Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:14:45.120045Z'}
INFO stampbot.webhook_handler:webhook_handler.py:974 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'existing_review_ids': [777]}, 'event': 'PR #42 in octocat/hello-world already has active approval for current head, skipping', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:14:45.120275Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'dismiss_approval' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42, 777, 'Duplicate Stampbot approval')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 71, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1090,7 +1090,7 @@
add_span_attributes(span, {"dismissal.reviews_found": len(review_ids)})
- if not review_ids:
+ if not not review_ids:
logger.debug(
"No Stampbot approvals to dismiss on PR #%d",
pr_number,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe7fffc88d0>
mock_github_client = <MagicMock name='github_client' id='140634406865952'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140634406866624'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:38:26.458725Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:38:26.459255Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:38:26.459519Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:38:26.458725Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:38:26.459255Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:38:26.459519Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 72, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1117,7 +1117,7 @@
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)
- status = "success" if success else "failure"
+ status = "success" if not success else "failure"
pr_dismissals_total.labels(trigger_type=trigger_type, status=status).inc()
add_span_attributes(span, {"dismissal.result": status})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
......................................................F
=================================== FAILURES ===================================
________________ test_pr_duplicate_dismissal_failure_is_counted ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc21ec87d10>
mock_github_client = <MagicMock name='github_client' id='140471717346816'>
@pytest.mark.asyncio
async def test_pr_duplicate_dismissal_failure_is_counted(webhook_handler, mock_github_client):
"""Test a dismissal that GitHub rejects is recorded without failing the approval."""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
],
]
mock_github_client.dismiss_approval.return_value = False
from stampbot.metrics import pr_dismissals_total
failures = pr_dismissals_total.labels(trigger_type="duplicate", status="failure")
before = failures._value.get()
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.dismiss_approval.assert_called_once()
> assert failures._value.get() == before + 1
E assert 5.0 == (5.0 + 1)
E + where 5.0 = get()
E + where get = <prometheus_client.values.MutexValue object at 0x7fc21ef37a70>.get
E + where <prometheus_client.values.MutexValue object at 0x7fc21ef37a70> = prometheus_client.metrics.Counter(stampbot_pr_dismissals)._value
tests/test_webhook_handler.py:908: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:25:24.688203Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:25:24.688739Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:25:24.688998Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:25:24.688203Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:25:24.688739Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:25:24.688998Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_duplicate_dismissal_failure_is_counted - assert 5.0 == (5.0 + 1)
+ where 5.0 = get()
+ where get = <prometheus_client.values.MutexValue object at 0x7fc21ef37a70>.get
+ where <prometheus_client.values.MutexValue object at 0x7fc21ef37a70> = prometheus_client.metrics.Counter(stampbot_pr_dismissals)._value
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 414 passed, 3 deselected, 1 warning in 4.47s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -50,7 +50,7 @@
NONE = "none"
-@dataclass(frozen=True)
+@dataclass(frozen=False)
class PullRequestEvent:
"""The parts of a pull request webhook that policy decisions read."""
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 1, definition_name: _refresh_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -379,7 +379,7 @@
add_span_attributes(span, {"webhook.result": "no_action"})
set_span_ok(span)
return dict(NO_ACTION)
- return await self._approve_for_label(span, event, label, skip_existing_check=True)
+ return await self._approve_for_label(span, event, label, skip_existing_check=False)
async def _approve_for_label(
self,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 2, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -989,7 +989,7 @@
},
)
set_span_ok(span)
- return True
+ return False
start_time = time.time()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fc3934ed940>
mock_github_client = <MagicMock name='github_client' id='140477960106544'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
> assert [r["status"] for r in results] == ["success", "success"]
E AssertionError: assert ['success', 'error'] == ['success', 'success']
E
E At index 1 diff: 'error' != 'success'
E
E Full diff:
E [
E 'success',
E - 'success',
E + 'error',
E ]
tests/test_webhook_handler.py:116: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:06:47.059739Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:06:47.060329Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:06:47.060615Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:06:47.061280Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:06:47.061560Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "existing_review_ids": [777]}, "event": "PR #42 in octocat/hello-world already has active approval for current head, skipping", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:06:47.061797Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:06:47.059739Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:06:47.060329Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:06:47.060615Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:06:47.061280Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:06:47.061560Z'}
INFO stampbot.webhook_handler:webhook_handler.py:974 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'existing_review_ids': [777]}, 'event': 'PR #42 in octocat/hello-world already has active approval for current head, skipping', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:06:47.061797Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: assert ['success', 'error'] == ['success', 'success']
At index 1 diff: 'error' != 'success'
Full diff:
[
'success',
- 'success',
+ 'error',
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 3, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1023,7 +1023,7 @@
"Duplicate Stampbot approval",
"duplicate",
head_sha=head_sha,
- keep_oldest=True,
+ keep_oldest=False,
)
add_span_attributes(span, {"approval.result": status})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fdb4fced940>
mock_github_client = <MagicMock name='github_client' id='140579908153904'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_not_called()
tests/test_webhook_handler.py:118:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140579908156592'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 777, 'Duplicate Stampbot approval')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:14.176131Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:49:14.176701Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:14.176967Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:14.177745Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:14.178012Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "existing_review_ids": [777]}, "event": "PR #42 in octocat/hello-world already has active approval for current head, skipping", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:14.178239Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:14.176131Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:49:14.176701Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:14.176967Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:14.177745Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:14.178012Z'}
INFO stampbot.webhook_handler:webhook_handler.py:974 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'existing_review_ids': [777]}, 'event': 'PR #42 in octocat/hello-world already has active approval for current head, skipping', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:14.178239Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'dismiss_approval' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42, 777, 'Duplicate Stampbot approval')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 4, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1083,7 +1083,7 @@
# Duplicate cleanup only touches approvals proven to be of
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
- reviews, head_sha, proven_only=True
+ reviews, head_sha, proven_only=False
)[1:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f848a3f16d0>
mock_github_client = <MagicMock name='github_client' id='140207225598512'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
> mock_github_client.dismiss_approval.assert_not_called()
tests/test_webhook_handler.py:300:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140207225586752'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 2, 'Duplicate Stampbot approval')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:50:19.643595Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:50:19.644208Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:50:19.643595Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:50:19.644208Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: Expected 'dismiss_approval' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42, 2, 'Duplicate Stampbot approval')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.28s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 5, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1099,7 +1099,7 @@
pr_dismissal_duration_seconds.observe(time.time() - start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
- return True
+ return False
# Dismiss each review
success = True........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..........................................F
=================================== FAILURES ===================================
_______________________ test_pr_unlabeled_no_bot_reviews _______________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fdf57c00ef0>
mock_github_client = <MagicMock name='github_client' id='140597227286080'>
@pytest.mark.asyncio
async def test_pr_unlabeled_no_bot_reviews(webhook_handler, mock_github_client):
"""Test removing label when no bot reviews exist."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [] # No reviews to dismiss
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:700: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:16:28.048848Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:16:28.049361Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:16:28.049528Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:16:28.048848Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:16:28.049361Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:16:28.049528Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_no_bot_reviews - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 402 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 6, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1102,7 +1102,7 @@
return True
# Dismiss each review
- success = True
+ success = False
for review_id in review_ids:
result = await run_in_threadpool(
github_client.dismiss_approval,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................F
=================================== FAILURES ===================================
________________________ test_pr_unlabeled_autoapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f5f51460c50>
mock_github_client = <MagicMock name='github_client' id='140047361300800'>
@pytest.mark.asyncio
async def test_pr_unlabeled_autoapprove(webhook_handler, mock_github_client):
"""Test removing autoapprove label dismisses approvals."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 222, "state": "APPROVED", "commit_id": "abc123def456"},
]
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:674: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:29:31.019782Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:29:31.020338Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:29:31.020495Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:29:31.019782Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:29:31.020338Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:29:31.020495Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_autoapprove - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 400 passed, 3 deselected, 1 warning in 4.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -112,7 +112,7 @@
True if signature is valid
"""
if not signature:
- return False
+ return True
expected_signature = (
"sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()........................................................................ [ 15%]
........................................................................ [ 31%]
......................................................................F
=================================== FAILURES ===================================
________________________ test_webhook_missing_signature ________________________
test_client = <starlette.testclient.TestClient object at 0x7f8a982406b0>
def test_webhook_missing_signature(test_client: TestClient):
"""Test webhook rejects requests without signature."""
response = test_client.post(
"/webhook",
json={"zen": "test"},
headers={"X-GitHub-Event": "ping"},
)
> assert response.status_code == 401
E assert 200 == 401
E + where 200 = <Response [200 OK]>.status_code
tests/test_main.py:274: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "ping", "action": ""}, "event": "Received ping event", "client_ip": "testclient", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:03:31.100131Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 200 OK\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:03:31.101086Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'ping', 'action': ''}, 'event': 'Received ping event', 'client_ip': 'testclient', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:03:31.100131Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 200 OK"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_missing_signature - assert 200 == 401
+ where 200 = <Response [200 OK]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 214 passed, 3 deselected, 1 warning in 3.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 1, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -121,7 +121,7 @@
try:
return hmac.compare_digest(expected_signature, signature)
except TypeError:
- return False
+ return True
async def handle_event(
self,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............F
=================================== FAILURES ===================================
_______________________ test_verify_signature_non_ascii ________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f09d2ed82f0>
def test_verify_signature_non_ascii(webhook_handler):
"""Test non-ASCII signature content is rejected."""
payload = b'{"test": "data"}'
signature = "sha256=\u2603"
> assert webhook_handler.verify_signature(payload, signature) is False
E assert True is False
E + where True = verify_signature(b'{"test": "data"}', 'sha256=☃')
E + where verify_signature = <stampbot.webhook_handler.WebhookHandler object at 0x7f09d2ed82f0>.verify_signature
tests/test_webhook_handler.py:1437: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_verify_signature_non_ascii - assert True is False
+ where True = verify_signature(b'{"test": "data"}', 'sha256=☃')
+ where verify_signature = <stampbot.webhook_handler.WebhookHandler object at 0x7f09d2ed82f0>.verify_signature
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 444 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 2, definition_name: _approve_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -386,7 +386,7 @@
span: Span | None,
event: PullRequestEvent,
label: str,
- skip_existing_check: bool = False,
+ skip_existing_check: bool = True,
) -> dict[str, Any]:
"""Approve a pull request that carries an approval label, if it is eligible.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f69e7731940>
mock_github_client = <MagicMock name='github_client' id='140092826792496'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140092826790816'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:11:55.980195Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:11:55.980811Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:11:55.981087Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:11:55.981674Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:11:55.981938Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:11:55.980195Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:11:55.980811Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:11:55.981087Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:11:55.981674Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:11:55.981938Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 3, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -531,7 +531,7 @@
event.repo_full_name,
label,
)
- if label_exists is False:
+ if label_exists is True:
logger.warning(
"Approval label %s not found in %s",
label,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 4, definition_name: _approval_needs_refresh
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -554,7 +554,7 @@
True when a prior approval exists but no active approval applies to the current head.
"""
if not reviews:
- return False
+ return True
return not self._has_current_active_approval(reviews, head_sha)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
....................F
=================================== FAILURES ===================================
_________ test_pr_labeled_unrelated_label_with_approval_label_ignored __________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fefff4163f0>
mock_github_client = <MagicMock name='github_client' id='140668751782848'>
@pytest.mark.asyncio
async def test_pr_labeled_unrelated_label_with_approval_label_ignored(
webhook_handler, mock_github_client
):
"""Test unrelated label changes do not re-approve just because an approval label exists."""
payload = load_fixture("pr_labeled_autoapprove")
payload["label"] = {"name": "bug"}
payload["pull_request"]["labels"].append({"name": "bug"})
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "ignored"
E AssertionError: assert 'success' == 'ignored'
E
E - ignored
E + success
tests/test_webhook_handler.py:496: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:11:35.180379Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:11:35.180956Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:11:35.181323Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:11:35.180379Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:11:35.180956Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:11:35.181323Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_unrelated_label_with_approval_label_ignored - AssertionError: assert 'success' == 'ignored'
- ignored
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 380 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 5, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -563,7 +563,7 @@
reviews: list[BotReview],
head_sha: str | None,
*,
- proven_only: bool = False,
+ proven_only: bool = True,
) -> list[int]:
"""Return the IDs of Stampbot approvals that cover the current head, oldest first.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
......................................................................F
=================================== FAILURES ===================================
___________ test_active_approvals_for_head[reviews4-None-expected4] ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa216be56d0>
reviews = [{'id': 1, 'state': 'APPROVED', 'commit_id': None}, {'id': 2, 'state': 'APPROVED', 'commit_id': 'head'}]
head_sha = None, expected = [1, 2]
@pytest.mark.parametrize(
("reviews", "head_sha", "expected"),
[
([], "head", []),
([{"id": 1, "state": "APPROVED", "commit_id": "head"}], "head", [1]),
(
[
{"id": 3, "state": "APPROVED", "commit_id": "head"},
{"id": 1, "state": "APPROVED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[1, 2, 3],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": "old"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": None},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
None,
[1, 2],
),
(
[
{"id": 1, "state": "DISMISSED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
],
)
def test_active_approvals_for_head(webhook_handler, reviews, head_sha, expected):
"""Test which approvals count as covering the head, oldest first."""
> assert webhook_handler._active_approvals_for_head(reviews, head_sha) == expected
E assert [] == [1, 2]
E
E Right contains 2 more items, first extra item: 1
E
E Full diff:
E + []
E - [
E - 1,
E - 2,
E - ]
tests/test_webhook_handler.py:256: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_active_approvals_for_head[reviews4-None-expected4] - assert [] == [1, 2]
Right contains 2 more items, first extra item: 1
Full diff:
+ []
- [
- 1,
- 2,
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 358 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 6, definition_name: _approve_pr
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -919,7 +919,7 @@
pr_number: int,
comment: str,
trigger_type: str,
- skip_existing_check: bool = False,
+ skip_existing_check: bool = True,
head_sha: str | None = None,
) -> bool:
"""Approve a PR and track metrics......................................................................... [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...................................................F
=================================== FAILURES ===================================
__________________ test_issue_comment_approve_skips_duplicate __________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f3e457c4470>
mock_github_client = <MagicMock name='github_client' id='139905427834592'>
@pytest.mark.asyncio
async def test_issue_comment_approve_skips_duplicate(webhook_handler, mock_github_client):
"""Test chatops approve skips duplicate when PR already approved."""
payload = load_fixture("issue_comment_approve")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 12345, "state": "APPROVED", "commit_id": "current-head"}
]
result = await webhook_handler.handle_event("issue_comment", payload)
assert result["status"] == "success"
mock_github_client.get_pr_head_sha.assert_called_once()
mock_github_client.find_bot_approval_reviews.assert_called_once()
> mock_github_client.approve_pr.assert_not_called()
tests/test_webhook_handler.py:848:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='139905427833920'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Approved by @maintainer via chatops')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:21:50.759675Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:21:50.760198Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:21:50.759675Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:21:50.760198Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_issue_comment_approve_skips_duplicate - AssertionError: Expected 'approve_pr' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Approved by @maintainer via chatops')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 411 passed, 3 deselected, 1 warning in 4.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 7, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1040,7 +1040,7 @@
trigger_type: str,
*,
head_sha: str | None = None,
- keep_oldest: bool = False,
+ keep_oldest: bool = True,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................F
=================================== FAILURES ===================================
________________________ test_pr_unlabeled_autoapprove _________________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fbb6168d730>
mock_github_client = <MagicMock name='github_client' id='140442773395776'>
@pytest.mark.asyncio
async def test_pr_unlabeled_autoapprove(webhook_handler, mock_github_client):
"""Test removing autoapprove label dismisses approvals."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = [
{"id": 111, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 222, "state": "APPROVED", "commit_id": "abc123def456"},
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
assert "dismissed" in result["message"].lower()
mock_github_client.find_bot_approval_reviews.assert_called_once()
> assert mock_github_client.dismiss_approval.call_count == 2
E AssertionError: assert 0 == 2
E + where 0 = <MagicMock name='github_client.dismiss_approval' id='140442773401152'>.call_count
E + where <MagicMock name='github_client.dismiss_approval' id='140442773401152'> = <MagicMock name='github_client' id='140442773395776'>.dismiss_approval
tests/test_webhook_handler.py:677: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:23:35.584713Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:23:35.585238Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:23:35.585371Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:23:35.584713Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:23:35.585238Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:23:35.585371Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_autoapprove - AssertionError: assert 0 == 2
+ where 0 = <MagicMock name='github_client.dismiss_approval' id='140442773401152'>.call_count
+ where <MagicMock name='github_client.dismiss_approval' id='140442773401152'> = <MagicMock name='github_client' id='140442773395776'>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 400 passed, 3 deselected, 1 warning in 4.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 8, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1142,7 +1142,7 @@
errors_total.labels(error_type="dismiss_failed").inc()
set_span_error(span, e)
- return False
+ return True
# Global handler instance........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f48d7c72990>
mock_github_client = <MagicMock name='github_client' id='139950745630912'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "error"
E AssertionError: assert 'success' == 'error'
E
E - error
E + success
tests/test_webhook_handler.py:210: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:51:11.250653Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:51:11.251175Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "Approval label autoapprove removed from PR #42", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:51:11.251308Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "Stampbot reviews could not be listed"}, "event": "Error dismissing approvals: Stampbot reviews could not be listed", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T20:51:11.251579Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:51:11.250653Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:51:11.251175Z'}
INFO stampbot.webhook_handler:webhook_handler.py:486 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'Approval label autoapprove removed from PR #42', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:51:11.251308Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': 'Stampbot reviews could not be listed'}, 'event': 'Error dismissing approvals: Stampbot reviews could not be listed', 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T20:51:11.251579Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - AssertionError: assert 'success' == 'error'
- error
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 0, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -275,7 +275,7 @@
)
decision, label = self._decide(event)
- if decision is Decision.APPROVE and label:
+ if decision is Decision.APPROVE or label:
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f402bef8aa0>
mock_github_client = <MagicMock name='github_client' id='139913579919552'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "error"
E AssertionError: assert 'success' == 'error'
E
E - error
E + success
tests/test_webhook_handler.py:210: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:58:17.711301Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:58:17.711862Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:58:17.712119Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42}, "event": "Could not read reviews for PR #42 in octocat/hello-world; approving without a check", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:58:17.712354Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "Stampbot reviews could not be listed"}, "event": "Error dismissing approvals: Stampbot reviews could not be listed", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T20:58:17.712738Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:58:17.711301Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:58:17.711862Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:58:17.712119Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:965 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42}, 'event': 'Could not read reviews for PR #42 in octocat/hello-world; approving without a check', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:58:17.712354Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': 'Stampbot reviews could not be listed'}, 'event': 'Error dismissing approvals: Stampbot reviews could not be listed', 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T20:58:17.712738Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - AssertionError: assert 'success' == 'error'
- error
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 1, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -277,7 +277,7 @@
decision, label = self._decide(event)
if decision is Decision.APPROVE and label:
result = await self._approve_for_label(span, event, label)
- elif decision is Decision.REFRESH and label:
+ elif decision is Decision.REFRESH or label:
result = await self._refresh_for_label(span, event, label)
elif decision is Decision.DISMISS and label:
result = await self._dismiss_for_label(span, event, label)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................................F
=================================== FAILURES ===================================
_________________ test_pr_unlabeled_unknown_review_state_fails _________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7ff58a0f4aa0>
mock_github_client = <MagicMock name='github_client' id='140692548155584'>
@pytest.mark.asyncio
async def test_pr_unlabeled_unknown_review_state_fails(webhook_handler, mock_github_client):
"""Test a dismissal cannot succeed when the review list is unavailable."""
payload = load_fixture("pr_unlabeled_autoapprove")
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "error"
E AssertionError: assert 'success' == 'error'
E
E - error
E + success
tests/test_webhook_handler.py:210: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "unlabeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:24:14.168844Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:24:14.169373Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42}, "event": "Could not read reviews for PR #42 in octocat/hello-world; approving without a refresh check", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T20:24:14.169661Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:24:14.169879Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "error": "Stampbot reviews could not be listed"}, "event": "Error dismissing approvals: Stampbot reviews could not be listed", "logger": "stampbot.webhook_handler", "level": "error", "timestamp": "2026-09-04T20:24:14.170231Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'unlabeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:24:14.168844Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:24:14.169373Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:372 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42}, 'event': 'Could not read reviews for PR #42 in octocat/hello-world; approving without a refresh check', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T20:24:14.169661Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:24:14.169879Z'}
ERROR stampbot.webhook_handler:webhook_handler.py:1133 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'error': 'Stampbot reviews could not be listed'}, 'event': 'Error dismissing approvals: Stampbot reviews could not be listed', 'logger': 'stampbot.webhook_handler', 'level': 'error', 'timestamp': '2026-09-04T20:24:14.170231Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_unlabeled_unknown_review_state_fails - AssertionError: assert 'success' == 'error'
- error
+ success
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 353 passed, 3 deselected, 1 warning in 4.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceAndWithOr, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -279,7 +279,7 @@
result = await self._approve_for_label(span, event, label)
elif decision is Decision.REFRESH and label:
result = await self._refresh_for_label(span, event, label)
- elif decision is Decision.DISMISS and label:
+ elif decision is Decision.DISMISS or label:
result = await self._dismiss_for_label(span, event, label)
else:
add_span_attributes(span, {"webhook.result": "no_action"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 3, definition_name: _decide
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -327,7 +327,7 @@
return Decision.APPROVE, label
return Decision.REFRESH, label
- if event.action == "synchronize" and config.reapprove:
+ if event.action == "synchronize" or config.reapprove:
return Decision.REFRESH, label
return Decision.NONE, None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
......................F
=================================== FAILURES ===================================
____________________ test_pr_synchronize_reapprove_disabled ____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe087445670>
mock_github_client = <MagicMock name='github_client' id='140602316979712'>
@pytest.mark.asyncio
async def test_pr_synchronize_reapprove_disabled(webhook_handler, mock_github_client):
"""Test new commits do not re-approve by default."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "ignored"
assert "no action" in result["message"].lower()
> mock_github_client.find_bot_approval_reviews.assert_not_called()
tests/test_webhook_handler.py:531:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='140602318742464'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'find_bot_approval_reviews' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:45:24.806351Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:45:24.806964Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:45:24.806351Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:45:24.806964Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_reapprove_disabled - AssertionError: Expected 'find_bot_approval_reviews' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 382 passed, 3 deselected, 1 warning in 4.26s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 4, definition_name: _approve_for_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -404,7 +404,7 @@
pr_author = event.pr.get("user", {}).get("login", "")
author_team_slugs: list[str] | None = None
- if config.needs_team_check(pr_author) and event.owner_login:
+ if config.needs_team_check(pr_author) or event.owner_login:
author_team_slugs = await run_in_threadpool(
github_client.get_user_team_slugs,
event.installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................F
=================================== FAILURES ===================================
_________________ test_pr_with_allowed_users_skips_team_check __________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f597e253290>
mock_github_client = <MagicMock name='github_client' id='140022344801584'>
@pytest.mark.asyncio
async def test_pr_with_allowed_users_skips_team_check(webhook_handler, mock_github_client):
"""Test PR with user in allowed_users skips team membership check."""
payload = load_fixture("pr_labeled_autoapprove")
# The fixture has user "contributor"
mock_github_client.get_repo_file.return_value = """
allowed_users = ["contributor"]
allowed_teams = ["acme/release-team"]
"""
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
# Should NOT call get_user_team_slugs since user is in allowed_users
> mock_github_client.get_user_team_slugs.assert_not_called()
tests/test_webhook_handler.py:1751:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_user_team_slugs' id='140022343185616'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_user_team_slugs' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat', 'contributor', ['acme/release-team'])].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:56:40.978213Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:56:40.979048Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:56:40.978213Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:56:40.979048Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_with_allowed_users_skips_team_check - AssertionError: Expected 'get_user_team_slugs' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat', 'contributor', ['acme/release-team'])].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 460 passed, 3 deselected, 1 warning in 5.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 5, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -586,7 +586,7 @@
def covers_head(review: BotReview) -> bool:
commit = review.get("commit_id")
if proven_only:
- return bool(head_sha) and commit == head_sha
+ return bool(head_sha) or commit == head_sha
return not head_sha or not commit or commit == head_sha
return sorted(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f65a3bb48d0>
mock_github_client = <MagicMock name='github_client' id='140074512092192'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140074512092864'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
E call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:07:06.678722Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:07:06.679246Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:07:06.679514Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:07:06.678722Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:07:06.679246Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:07:06.679514Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 6, definition_name: _active_approvals_for_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -592,7 +592,7 @@
return sorted(
review["id"]
for review in reviews
- if review["state"] == "APPROVED" and covers_head(review)
+ if review["state"] == "APPROVED" or covers_head(review)
)
def _has_current_active_approval(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa13f6e08d0>
mock_github_client = <MagicMock name='github_client' id='140330528263200'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='140330528263872'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 3 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 60, 'Duplicate Stampbot approval'),
E call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
E call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:51:40.831877Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:51:40.832458Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:51:40.832737Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:51:40.831877Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:51:40.832458Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:51:40.832737Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 3 times.
Calls: [call(12345, 'octocat/hello-world', 42, 60, 'Duplicate Stampbot approval'),
call(12345, 'octocat/hello-world', 42, 100, 'Duplicate Stampbot approval'),
call(12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 7, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1112,7 +1112,7 @@
review_id,
message,
)
- success = success and result
+ success = success or result
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
......................................................F
=================================== FAILURES ===================================
________________ test_pr_duplicate_dismissal_failure_is_counted ________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f539be9d610>
mock_github_client = <MagicMock name='github_client' id='139997146410560'>
@pytest.mark.asyncio
async def test_pr_duplicate_dismissal_failure_is_counted(webhook_handler, mock_github_client):
"""Test a dismissal that GitHub rejects is recorded without failing the approval."""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
],
]
mock_github_client.dismiss_approval.return_value = False
from stampbot.metrics import pr_dismissals_total
failures = pr_dismissals_total.labels(trigger_type="duplicate", status="failure")
before = failures._value.get()
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.dismiss_approval.assert_called_once()
> assert failures._value.get() == before + 1
E assert 4.0 == (4.0 + 1)
E + where 4.0 = get()
E + where get = <prometheus_client.values.MutexValue object at 0x7f53a0121eb0>.get
E + where <prometheus_client.values.MutexValue object at 0x7f53a0121eb0> = prometheus_client.metrics.Counter(stampbot_pr_dismissals)._value
tests/test_webhook_handler.py:908: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:38:53.463033Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:38:53.463574Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:38:53.463836Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:38:53.463033Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:38:53.463574Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:38:53.463836Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_duplicate_dismissal_failure_is_counted - assert 4.0 == (4.0 + 1)
+ where 4.0 = get()
+ where get = <prometheus_client.values.MutexValue object at 0x7f53a0121eb0>.get
+ where <prometheus_client.values.MutexValue object at 0x7f53a0121eb0> = prometheus_client.metrics.Counter(stampbot_pr_dismissals)._value
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 414 passed, 3 deselected, 1 warning in 4.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 0, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -194,7 +194,7 @@
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
- repo_default_branch = repo.get("default_branch") or "main"
+ repo_default_branch = repo.get("default_branch") and "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
..............F
=================================== FAILURES ===================================
______________ test_repo_config_cache_key_includes_default_branch ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f8389101370>
mock_github_client = <MagicMock name='github_client' id='140202918235840'>
@pytest.mark.asyncio
async def test_repo_config_cache_key_includes_default_branch(webhook_handler, mock_github_client):
"""Test policy read from another default branch is not served from the cache."""
payload = load_fixture("pr_labeled_autoapprove")
await webhook_handler.handle_event("pull_request", payload)
payload["repository"]["default_branch"] = "develop"
await webhook_handler.handle_event("pull_request", payload)
> assert mock_github_client.get_repo_file.call_count == 2
E AssertionError: assert 1 == 2
E + where 1 = <MagicMock name='github_client.get_repo_file' id='140202917515952'>.call_count
E + where <MagicMock name='github_client.get_repo_file' id='140202917515952'> = <MagicMock name='github_client' id='140202918235840'>.get_repo_file
tests/test_webhook_handler.py:414: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:43:17.488269Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:43:17.488819Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:43:17.489071Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:43:17.489581Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:43:17.489823Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:43:17.488269Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:43:17.488819Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:43:17.489071Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:43:17.489581Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:43:17.489823Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_cache_key_includes_default_branch - AssertionError: assert 1 == 2
+ where 1 = <MagicMock name='github_client.get_repo_file' id='140202917515952'>.call_count
+ where <MagicMock name='github_client.get_repo_file' id='140202917515952'> = <MagicMock name='github_client' id='140202918235840'>.get_repo_file
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 374 passed, 3 deselected, 1 warning in 4.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 1, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -203,7 +203,7 @@
with create_span(
"webhook.handle_pull_request",
{
- "github.repo": repo_full_name or "unknown",
+ "github.repo": repo_full_name and "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -204,7 +204,7 @@
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
- "github.pr_number": pr_number or 0,
+ "github.pr_number": pr_number and 0,
"github.action": action or "unknown",
},
) as span:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 3, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -205,7 +205,7 @@
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
- "github.action": action or "unknown",
+ "github.action": action and "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 4, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha and not commit or commit == head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
......................................................................F
=================================== FAILURES ===================================
___________ test_active_approvals_for_head[reviews4-None-expected4] ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f002b9dd6d0>
reviews = [{'id': 1, 'state': 'APPROVED', 'commit_id': None}, {'id': 2, 'state': 'APPROVED', 'commit_id': 'head'}]
head_sha = None, expected = [1, 2]
@pytest.mark.parametrize(
("reviews", "head_sha", "expected"),
[
([], "head", []),
([{"id": 1, "state": "APPROVED", "commit_id": "head"}], "head", [1]),
(
[
{"id": 3, "state": "APPROVED", "commit_id": "head"},
{"id": 1, "state": "APPROVED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[1, 2, 3],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": "old"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
(
[
{"id": 1, "state": "APPROVED", "commit_id": None},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
None,
[1, 2],
),
(
[
{"id": 1, "state": "DISMISSED", "commit_id": "head"},
{"id": 2, "state": "APPROVED", "commit_id": "head"},
],
"head",
[2],
),
],
)
def test_active_approvals_for_head(webhook_handler, reviews, head_sha, expected):
"""Test which approvals count as covering the head, oldest first."""
> assert webhook_handler._active_approvals_for_head(reviews, head_sha) == expected
E assert [1] == [1, 2]
E
E Right contains one more item: 2
E
E Full diff:
E [
E 1,
E - 2,
E ]
tests/test_webhook_handler.py:256: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_active_approvals_for_head[reviews4-None-expected4] - assert [1] == [1, 2]
Right contains one more item: 2
Full diff:
[
1,
- 2,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 358 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 5, definition_name: covers_head
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -587,7 +587,7 @@
commit = review.get("commit_id")
if proven_only:
return bool(head_sha) and commit == head_sha
- return not head_sha or not commit or commit == head_sha
+ return not head_sha or not commit and commit == head_sha
return sorted(
review["id"]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f55e255d940>
mock_github_client = <MagicMock name='github_client' id='140006840283696'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140006840281680'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:40:51.312910Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:40:51.313489Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:40:51.313751Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:40:51.314383Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:40:51.314660Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:40:51.312910Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:40:51.313489Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:40:51.313751Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:40:51.314383Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:40:51.314660Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 6, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -645,7 +645,7 @@
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
- repo_default_branch = repo.get("default_branch") or "main"
+ repo_default_branch = repo.get("default_branch") and "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 7, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -655,7 +655,7 @@
with create_span(
"webhook.handle_chatops",
{
- "github.repo": repo_full_name or "unknown",
+ "github.repo": repo_full_name and "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceContinueWithBreak, occurrence: 0, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -524,7 +524,7 @@
present = event.labels
for label in event.repo_config.approval_labels:
if label in present:
- continue
+ break
label_exists = await run_in_threadpool(
github_client.repo_has_label,
event.installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......F
=================================== FAILURES ===================================
__________________ test_pr_opened_missing_label_logs_warning ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f3ed7c255c0>
mock_github_client = <MagicMock name='github_client' id='139907879587184'>
@pytest.mark.asyncio
async def test_pr_opened_missing_label_logs_warning(webhook_handler, mock_github_client):
"""Test missing approval label triggers label check after the review decision."""
payload = load_fixture("pr_opened_with_autoapprove_label")
mock_github_client.repo_has_label.return_value = False
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
# The label on the pull request exists by definition; only "stamp" is checked.
> mock_github_client.repo_has_label.assert_called_once_with(12345, "octocat/hello-world", "stamp")
tests/test_webhook_handler.py:352:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.repo_has_label' id='139907879591552'>
args = (12345, 'octocat/hello-world', 'stamp'), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:57:33.152901Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:57:33.153438Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:57:33.153735Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:57:33.152901Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:57:33.153438Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:57:33.153735Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_missing_label_logs_warning - AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 367 passed, 3 deselected, 1 warning in 4.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 0, definition_name: verify_signature
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -120,7 +120,7 @@
try:
return hmac.compare_digest(expected_signature, signature)
- except TypeError:
+ except CosmicRayTestingException:
return False
async def handle_event(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............F
=================================== FAILURES ===================================
_______________________ test_verify_signature_non_ascii ________________________
self = <stampbot.webhook_handler.WebhookHandler object at 0x7efe41468350>
payload = b'{"test": "data"}', signature = 'sha256=☃'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
"sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:
> return hmac.compare_digest(expected_signature, signature)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: comparing strings with non-ASCII characters is not supported
stampbot/webhook_handler.py:122: TypeError
During handling of the above exception, another exception occurred:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7efe41468350>
def test_verify_signature_non_ascii(webhook_handler):
"""Test non-ASCII signature content is rejected."""
payload = b'{"test": "data"}'
signature = "sha256=\u2603"
> assert webhook_handler.verify_signature(payload, signature) is False
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1437:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7efe41468350>
payload = b'{"test": "data"}', signature = 'sha256=☃'
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature.
Args:
payload: Request payload
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid
"""
if not signature:
return False
expected_signature = (
"sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
)
try:
return hmac.compare_digest(expected_signature, signature)
> except CosmicRayTestingException:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/webhook_handler.py:123: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_verify_signature_non_ascii - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 444 passed, 3 deselected, 1 warning in 5.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 1, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1125,7 +1125,7 @@
return success
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)
pr_dismissals_total.labels(trigger_type=trigger_type, status="failure").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.............................................................F
=================================== FAILURES ===================================
___ test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] ____
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fe1f1ccdbf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
> reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
stampbot/webhook_handler.py:1072:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/concurrency.py:34: in run_in_threadpool
return await anyio.to_thread.run_sync(func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/to_thread.py:63: in run_sync
return await get_async_backend().run_sync_in_worker_thread(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:2596: in run_sync_in_worker_thread
return await future
^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1029: in run
result = context.run(func, *args)
^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.find_bot_approval_reviews' id='140608398823104'>
args = (12345, 'octocat/hello-world', 42), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E RuntimeError: GitHub unavailable
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: RuntimeError
During handling of the above exception, another exception occurred:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe1f1ccdbf0>
mock_github_client = <MagicMock name='github_client' id='140608398831168'>
failure = RuntimeError('GitHub unavailable')
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", [RuntimeError("GitHub unavailable"), None])
async def test_pr_duplicate_check_failure_keeps_approval_and_is_counted(
webhook_handler, mock_github_client, failure
):
"""Test a failed duplicate scan keeps the approval and reaches the failure counter.
The client reports a listing failure as None; an unexpected exception is
handled the same way.
"""
from stampbot.metrics import pr_dismissals_total
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [[], failure]
failures = pr_dismissals_total.labels(trigger_type="duplicate", status="failure")
before = failures._value.get()
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:166:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:449: in _approve_for_label
success = await self._approve_pr(
stampbot/webhook_handler.py:1019: in _approve_pr
await self._dismiss_approvals(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fe1f1ccdbf0>
installation_id = 12345, repo_full_name = 'octocat/hello-world', pr_number = 42
message = 'Duplicate Stampbot approval', trigger_type = 'duplicate'
head_sha = 'abc123def456', keep_oldest = True
async def _dismiss_approvals(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
trigger_type: str,
*,
head_sha: str | None = None,
keep_oldest: bool = False,
) -> bool:
"""Dismiss active Stampbot approvals on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
pr_number: PR number
message: Dismissal message
trigger_type: What triggered the dismissal (label_removed, chatops, duplicate)
head_sha: Only approvals covering this head are dismissed. None means every
active approval.
keep_oldest: Leave the oldest matching approval in place, which turns the
call into duplicate cleanup.
Returns:
True if every selected approval was dismissed, or none needed dismissal.
"""
with create_span(
"webhook.dismiss_approvals",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"dismissal.trigger_type": trigger_type,
},
) as span:
start_time = time.time()
try:
reviews = await run_in_threadpool(
github_client.find_bot_approval_reviews,
installation_id,
repo_full_name,
pr_number,
)
if reviews is None:
# Nothing can be dismissed without the list, and the
# failure has to reach the metric operators watch.
raise RuntimeError("Stampbot reviews could not be listed")
if keep_oldest:
# Duplicate cleanup only touches approvals proven to be of
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
reviews, head_sha, proven_only=True
)[1:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)
add_span_attributes(span, {"dismissal.reviews_found": len(review_ids)})
if not review_ids:
logger.debug(
"No Stampbot approvals to dismiss on PR #%d",
pr_number,
extra={"repo": repo_full_name, "pr_number": pr_number},
)
pr_dismissal_duration_seconds.observe(time.time() - start_time)
add_span_attributes(span, {"dismissal.result": "no_reviews"})
set_span_ok(span)
return True
# Dismiss each review
success = True
for review_id in review_ids:
result = await run_in_threadpool(
github_client.dismiss_approval,
installation_id,
repo_full_name,
pr_number,
review_id,
message,
)
success = success and result
duration = time.time() - start_time
pr_dismissal_duration_seconds.observe(duration)
status = "success" if success else "failure"
pr_dismissals_total.labels(trigger_type=trigger_type, status=status).inc()
add_span_attributes(span, {"dismissal.result": status})
set_span_ok(span)
return success
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/webhook_handler.py:1128: NameError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:54:07.114130Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:54:07.114692Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:54:07.114951Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:54:07.114130Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:54:07.114692Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:54:07.114951Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_duplicate_check_failure_keeps_approval_and_is_counted[failure0] - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 349 passed, 3 deselected, 1 warning in 4.21s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/webhook_handler.py +++ b/stampbot/webhook_handler.py @@ -32,7 +32,7 @@ logger = get_logger(__name__) # Security: Maximum length for user-controlled strings to prevent DoS -MAX_COMMENT_LENGTH = 65536 # 64KB - generous but prevents abuse +MAX_COMMENT_LENGTH = 65537 # 64KB - generous but prevents abuse # Pull request actions that can change Stampbot's review state. Every other # action returns before any GitHub request is made.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 1, definition_name: None
--- mutation diff --- --- a/stampbot/webhook_handler.py +++ b/stampbot/webhook_handler.py @@ -32,7 +32,7 @@ logger = get_logger(__name__) # Security: Maximum length for user-controlled strings to prevent DoS -MAX_COMMENT_LENGTH = 65536 # 64KB - generous but prevents abuse +MAX_COMMENT_LENGTH = 65535 # 64KB - generous but prevents abuse # Pull request actions that can change Stampbot's review state. Every other # action returns before any GitHub request is made.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 2, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -204,7 +204,7 @@
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
- "github.pr_number": pr_number or 0,
+ "github.pr_number": pr_number or 1,
"github.action": action or "unknown",
},
) as span:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 3, definition_name: _handle_pull_request
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -204,7 +204,7 @@
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
- "github.pr_number": pr_number or 0,
+ "github.pr_number": pr_number or -1,
"github.action": action or "unknown",
},
) as span:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 4, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -637,7 +637,7 @@
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
- pr_number = int(pr_url.split("/")[-1])
+ pr_number = int(pr_url.split("/")[- 2])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f918ceec200>
mock_github_client = <MagicMock name='github_client' id='140263104459312'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f918ceec200>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
> pr_number = int(pr_url.split("/")[- 2])
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E ValueError: invalid literal for int() with base 10: 'pulls'
stampbot/webhook_handler.py:640: ValueError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:30:10.665807Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:30:10.665807Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - ValueError: invalid literal for int() with base 10: 'pulls'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 5, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -637,7 +637,7 @@
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
- pr_number = int(pr_url.split("/")[-1])
+ pr_number = int(pr_url.split("/")[- 0])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f12d51eea80>
mock_github_client = <MagicMock name='github_client' id='139718852784688'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f12d51eea80>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
> pr_number = int(pr_url.split("/")[- 0])
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E ValueError: invalid literal for int() with base 10: 'https:'
stampbot/webhook_handler.py:640: ValueError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:28:57.991724Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:28:57.991724Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - ValueError: invalid literal for int() with base 10: 'https:'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 6, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -701,7 +701,7 @@
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
- command = command_match.group(1).lower()
+ command = command_match.group( 2).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f4f284fab10>
mock_github_client = <MagicMock name='github_client' id='139977878565424'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
> result = await webhook_handler.handle_event("issue_comment", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:170: in handle_event
result = await self._handle_pr_comment(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f4f284fab10>
payload = {'action': 'created', 'issue': {'url': 'https://api.github.com/repos/octocat/hello-world/issues/42', 'repository_url':...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pr_comment(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle PR comment events for chatops.
Args:
payload: Event payload
Returns:
Response dictionary
"""
comment = payload.get("comment", {})
comment_body = comment.get("body", "")
# Security: limit comment length to prevent DoS
if len(comment_body) > MAX_COMMENT_LENGTH:
return {"status": "ignored", "message": "Comment too long"}
comment_body = comment_body.lower().strip()
# Check if bot is mentioned
if "@stampbot" not in comment_body:
return {"status": "ignored", "message": "Bot not mentioned"}
# Extract PR info
if "pull_request" in payload.get("issue", {}):
pr_url = payload["issue"]["pull_request"]["url"]
# Extract PR number from URL
pr_number = int(pr_url.split("/")[-1])
elif "pull_request" in payload:
pr_number = payload["pull_request"]["number"]
else:
return {"status": "error", "message": "Not a PR comment"}
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
commenter = comment.get("user", {}).get("login", "unknown")
with create_span(
"webhook.handle_chatops",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number,
"chatops.commenter": commenter,
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in comment event")
add_span_attributes(span, {"chatops.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
add_span_attributes(
span,
{
"chatops.result": "config_error",
"chatops.config_error": repo_config.config_error,
},
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
if not repo_config.chatops_enabled:
add_span_attributes(span, {"chatops.result": "disabled"})
set_span_ok(span)
return {"status": "ignored", "message": "Chatops not enabled"}
# Parse command
command_match = re.search(r"@stampbot\s+(\w+)", comment_body)
if not command_match:
chatops_commands_total.labels(command="none", status="ignored").inc()
add_span_attributes(span, {"chatops.result": "no_command"})
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
> command = command_match.group( 2).lower()
^^^^^^^^^^^^^^^^^^^^^^^
E IndexError: no such group
stampbot/webhook_handler.py:704: IndexError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:18:41.438250Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:18:41.438995Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:18:41.438250Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:18:41.438995Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - IndexError: no such group
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 7, definition_name: _handle_pr_comment
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -701,7 +701,7 @@
set_span_ok(span)
return {"status": "ignored", "message": "No command found"}
- command = command_match.group(1).lower()
+ command = command_match.group( 0).lower()
add_span_attributes(span, {"chatops.command": command})
if command == "help":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...F
=================================== FAILURES ===================================
__________ test_chatops_approve_without_head_skips_duplicate_cleanup ___________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6276eed6d0>
mock_github_client = <MagicMock name='github_client' id='140060871799344'>
@pytest.mark.asyncio
async def test_chatops_approve_without_head_skips_duplicate_cleanup(
webhook_handler, mock_github_client
):
"""Test no dismissal happens when the head is unknown, even with several approvals."""
payload = load_fixture("issue_comment_approve")
mock_github_client.get_pr_head_sha.return_value = None
mock_github_client.find_bot_approval_reviews.side_effect = [
[],
[
{"id": 1, "state": "APPROVED", "commit_id": "a"},
{"id": 2, "state": "APPROVED", "commit_id": "b"},
],
]
result = await webhook_handler.handle_event("issue_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:299: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "issue_comment", "action": "created"}, "event": "Received issue_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:23:27.604998Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:23:27.605578Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'issue_comment', 'action': 'created'}, 'event': 'Received issue_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:23:27.604998Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:23:27.605578Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_chatops_approve_without_head_skips_duplicate_cleanup - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 363 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 8, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1084,7 +1084,7 @@
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
reviews, head_sha, proven_only=True
- )[1:]
+ )[ 2:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f4b073f48d0>
mock_github_client = <MagicMock name='github_client' id='139960218258464'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='139960218259136'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:29:20.819041Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:29:20.819582Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:29:20.819842Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:29:20.819041Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:29:20.819582Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:29:20.819842Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 9, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1084,7 +1084,7 @@
# this head, and leaves the oldest of them in place.
review_ids = self._active_approvals_for_head(
reviews, head_sha, proven_only=True
- )[1:]
+ )[ 0:]
else:
review_ids = self._active_approvals_for_head(reviews, head_sha)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f23537f9940>
mock_github_client = <MagicMock name='github_client' id='139789695857200'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_not_called()
tests/test_webhook_handler.py:118:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='139789695859888'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to not have been called. Called 1 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 777, 'Duplicate Stampbot approval')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:17.019763Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:20:17.020321Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:17.020600Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:17.021360Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:17.021631Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "existing_review_ids": [777]}, "event": "PR #42 in octocat/hello-world already has active approval for current head, skipping", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:17.021863Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:17.019763Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:20:17.020321Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:17.020600Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:17.021360Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:17.021631Z'}
INFO stampbot.webhook_handler:webhook_handler.py:974 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'existing_review_ids': [777]}, 'event': 'PR #42 in octocat/hello-world already has active approval for current head, skipping', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:17.021863Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'dismiss_approval' to not have been called. Called 1 times.
Calls: [call(12345, 'octocat/hello-world', 42, 777, 'Duplicate Stampbot approval')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -48,9 +48,6 @@
REFRESH = "refresh" # approve again only when a prior approval is stale
DISMISS = "dismiss"
NONE = "none"
-
-
-@dataclass(frozen=True)
class PullRequestEvent:
"""The parts of a pull request webhook that policy decisions read."""
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f466e6cf5c0>
mock_github_client = <MagicMock name='github_client' id='139940471453216'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:161: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f466e6cf5c0>
payload = {'action': 'opened', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pulls/42'...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
if repo_config.config_error:
logger.warning(
"Invalid Stampbot configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"webhook.result": "config_error",
"webhook.config_error": repo_config.config_error,
},
)
if action == "opened":
await run_in_threadpool(
github_client.create_pr_review_comment,
installation_id,
repo_full_name,
pr_number,
(
"Stampbot configuration error:\n\n"
f"{repo_config.config_error}\n\n"
"Please fix the configuration to re-enable automation."
),
)
set_span_ok(span)
return {
"status": "error",
"message": "Invalid repository configuration",
}
> event = PullRequestEvent(
action=action,
installation_id=installation_id,
repo_full_name=repo_full_name,
pr_number=pr_number,
owner_login=owner_login,
pr=pr,
event_label=payload.get("label", {}).get("name"),
repo_config=repo_config,
)
E TypeError: PullRequestEvent() takes no arguments
stampbot/webhook_handler.py:263: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:29:28.503893Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:29:28.504713Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:153 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:29:28.503893Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:29:28.504713Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: PullRequestEvent() takes no arguments
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 1, definition_name: PullRequestEvent
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -62,8 +62,6 @@
pr: dict[str, Any]
event_label: str | None
repo_config: RepoConfig
-
- @property
def labels(self) -> list[str]:
"""Names of the labels currently on the pull request."""
return [label["name"] for label in self.pr.get("labels", [])]........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fba7b84b5c0>
mock_github_client = <MagicMock name='github_client' id='140438907041312'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:162: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:275: in _handle_pull_request
decision, label = self._decide(event)
^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:312: in _decide
label = self._find_approval_label(event.labels, config)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7fba7b84b5c0>
labels = <bound method PullRequestEvent.labels of PullRequestEvent(action='opened', installation_id=12345, repo_full_name='octo...etions': 10, 'changed_files': 3}, event_label=None, repo_config=<stampbot.config.RepoConfig object at 0x7fba7b2d0710>)>
repo_config = <stampbot.config.RepoConfig object at 0x7fba7b2d0710>
def _find_approval_label(self, labels: list[str], repo_config: RepoConfig) -> str | None:
"""Find the first configured approval label present on the PR.
Args:
labels: Current PR labels.
repo_config: Repository configuration.
Returns:
Matching approval label, if present.
"""
for label in repo_config.approval_labels:
> if label in labels:
^^^^^^^^^^^^^^^
E TypeError: argument of type 'method' is not a container or iterable
stampbot/webhook_handler.py:344: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:10:17.389260Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:10:17.390084Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:154 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:10:17.389260Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:10:17.390084Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - TypeError: argument of type 'method' is not a container or iterable
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 2, definition_name: PullRequestEvent
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -67,8 +67,6 @@
def labels(self) -> list[str]:
"""Names of the labels currently on the pull request."""
return [label["name"] for label in self.pr.get("labels", [])]
-
- @property
def head_sha(self) -> str | None:
"""Current head commit, when GitHub included it."""
sha = self.pr.get("head", {}).get("sha")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................................F
=================================== FAILURES ===================================
_______________ test_pr_labeled_at_creation_approves_once[False] _______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6914df1940>
mock_github_client = <MagicMock name='github_client' id='140089292108336'>
labeled_first = False
@pytest.mark.asyncio
@pytest.mark.parametrize("labeled_first", [False, True])
async def test_pr_labeled_at_creation_approves_once(
webhook_handler, mock_github_client, labeled_first
):
"""Test the opened and labeled events for one new pull request yield one approval.
GitHub delivers the two events in either order. When the second event can
see the first approval, the existing-approval check stops it.
"""
opened = load_fixture("pr_opened_with_autoapprove_label")
labeled = load_fixture("pr_labeled_autoapprove")
events = [labeled, opened] if labeled_first else [opened, labeled]
_remember_approval(mock_github_client)
results = [await webhook_handler.handle_event("pull_request", e) for e in events]
assert [r["status"] for r in results] == ["success", "success"]
> mock_github_client.approve_pr.assert_called_once()
tests/test_webhook_handler.py:117:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.approve_pr' id='140089292106320'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
E call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:42:20.080516Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:42:20.081140Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:42:20.081411Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:42:20.082115Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:42:20.082371Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:154 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:42:20.080516Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:42:20.081140Z'}
INFO stampbot.webhook_handler:webhook_handler.py:440 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:42:20.081411Z'}
INFO stampbot.webhook_handler:webhook_handler.py:154 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:42:20.082115Z'}
INFO stampbot.webhook_handler:webhook_handler.py:440 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:42:20.082371Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_labeled_at_creation_approves_once[False] - AssertionError: Expected 'approve_pr' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)'),
call(12345, 'octocat/hello-world', 42, 'Auto-approved by Stampbot (label: autoapprove)')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 346 passed, 3 deselected, 1 warning in 4.29s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 3, definition_name: WebhookHandler
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -82,8 +82,6 @@
"""Initialize webhook handler (lazy initialization)."""
self._webhook_secret: bytes | None = None
self._policy = RepoPolicyResolver(repo_config_cache_seconds())
-
- @property
def webhook_secret(self) -> bytes:
"""Get webhook secret, initializing if needed.
........................................................................ [ 15%]
........................................................................ [ 31%]
.....................................................................F
=================================== FAILURES ===================================
_______________________ test_request_with_content_length _______________________
test_client = <starlette.testclient.TestClient object at 0x7f1e7f99f680>
def test_request_with_content_length(test_client: TestClient):
"""Test that requests with Content-Length header are tracked."""
# POST with a body will have Content-Length set automatically
> response = test_client.post(
"/webhook",
content=b'{"test": "data"}',
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": "sha256=invalid",
},
)
tests/test_main.py:254:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:555: in post
return super().post(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1144: in post
return self.request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:454: in request
return super().request(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:825: in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:914: in send
response = self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:942: in _send_handling_auth
response = self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:979: in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_client.py:1014: in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:356: in handle_request
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/testclient.py:353: in handle_request
portal.call(self.app, scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:338: in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:447: in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/concurrent/futures/_base.py:396: in __get_result
raise self._exception
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/from_thread.py:263: in _call_func
retval = await retval_or_awaitable
^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:1163: in __call__
await super().__call__(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/applications.py:90: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:186: in __call__
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/errors.py:164: in __call__
await self.app(scope, receive, _send)
stampbot/main.py:336: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:193: in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:251: in metrics_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:168: in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/base.py:144: in coro
await self.app(scope, receive_or_disconnect, send_no_error)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/middleware/exceptions.py:63: in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:660: in __call__
await self.middleware_stack(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:2734: in app
await route.handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:1281: in handle
await super().handle(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/routing.py:276: in handle
await self.app(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:158: in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:53: in wrapped_app
raise exc
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/_exception_handler.py:42: in wrapped_app
await app(scope, receive, sender)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:144: in app
response = await f(request)
^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:706: in app
raw_response = await run_endpoint_function(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/routing.py:352: in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/main.py:475: in webhook
if not webhook_handler.verify_signature(body, x_hub_signature_256):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:116: in verify_signature
"sha256=" + hmac.new(self.webhook_secret, payload, hashlib.sha256).hexdigest()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/hmac.py:218: in new
return HMAC(key, msg, digestmod)
^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <hmac.HMAC object at 0x7f1e7f5ae890>
key = <bound method WebhookHandler.webhook_secret of <stampbot.webhook_handler.WebhookHandler object at 0x7f1e7fed9010>>
msg = b'{"test": "data"}', digestmod = <built-in function openssl_sha256>
def __init__(self, key, msg=None, digestmod=''):
"""Create a new HMAC object.
key: bytes or buffer, key for the keyed hash object.
msg: bytes or buffer, Initial input for the hash or None.
digestmod: A hash name suitable for hashlib.new(). *OR*
A hashlib constructor returning a new hash object. *OR*
A module supporting PEP 247.
Required as of 3.8, despite its position after the optional
msg argument. Passing it as a keyword argument is
recommended, though not required for legacy API reasons.
"""
if not isinstance(key, (bytes, bytearray)):
> raise TypeError(f"key: expected bytes or bytearray, "
f"but got {type(key).__name__!r}")
E TypeError: key: expected bytes or bytearray, but got 'method'
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/hmac.py:71: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_request_with_content_length - TypeError: key: expected bytes or bytearray, but got 'method'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 213 passed, 3 deselected, 1 warning in 4.21s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 0, definition_name: _find_approval_label
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -342,7 +342,7 @@
Returns:
Matching approval label, if present.
"""
- for label in repo_config.approval_labels:
+ for label in []:
if label in labels:
return label
return None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fe52bcff5c0>
mock_github_client = <MagicMock name='github_client' id='140622258484768'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:77: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:23:09.037179Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:23:09.037939Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:23:09.037179Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:23:09.037939Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 3.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 1, definition_name: _warn_missing_approval_labels
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -522,7 +522,7 @@
event: Pull request event
"""
present = event.labels
- for label in event.repo_config.approval_labels:
+ for label in []:
if label in present:
continue
label_exists = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......F
=================================== FAILURES ===================================
__________________ test_pr_opened_missing_label_logs_warning ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f994b511e80>
mock_github_client = <MagicMock name='github_client' id='140296366329200'>
@pytest.mark.asyncio
async def test_pr_opened_missing_label_logs_warning(webhook_handler, mock_github_client):
"""Test missing approval label triggers label check after the review decision."""
payload = load_fixture("pr_opened_with_autoapprove_label")
mock_github_client.repo_has_label.return_value = False
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
# The label on the pull request exists by definition; only "stamp" is checked.
> mock_github_client.repo_has_label.assert_called_once_with(12345, "octocat/hello-world", "stamp")
tests/test_webhook_handler.py:352:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.repo_has_label' id='140296366333568'>
args = (12345, 'octocat/hello-world', 'stamp'), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:33:16.702630Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:33:16.703164Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:33:16.703416Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:33:16.702630Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:33:16.703164Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:33:16.703416Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_missing_label_logs_warning - AssertionError: Expected 'repo_has_label' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 367 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 2, definition_name: _dismiss_approvals
--- mutation diff ---
--- a/stampbot/webhook_handler.py
+++ b/stampbot/webhook_handler.py
@@ -1103,7 +1103,7 @@
# Dismiss each review
success = True
- for review_id in review_ids:
+ for review_id in []:
result = await run_in_threadpool(
github_client.dismiss_approval,
installation_id,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................................F
=================================== FAILURES ===================================
_____________ test_pr_opened_and_labeled_race_leaves_one_approval ______________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f27925d08d0>
mock_github_client = <MagicMock name='github_client' id='139807933697056'>
@pytest.mark.asyncio
async def test_pr_opened_and_labeled_race_leaves_one_approval(webhook_handler, mock_github_client):
"""Test two replicas that both approved converge on one active approval.
Neither replica saw the other's review before approving. After approving,
each re-reads the reviews and dismisses every approval of the head except
the oldest, so both reach the same answer.
"""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.find_bot_approval_reviews.side_effect = [
[], # pre-check: nothing yet
[ # post-approval scan: both replicas have posted
{"id": 200, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 100, "state": "APPROVED", "commit_id": "abc123def456"},
{"id": 50, "state": "APPROVED", "commit_id": "oldsha"}, # stale, not a duplicate
{"id": 60, "state": "DISMISSED", "commit_id": "abc123def456"},
],
]
result = await webhook_handler.handle_event("pull_request", payload)
assert result["status"] == "success"
mock_github_client.approve_pr.assert_called_once()
> mock_github_client.dismiss_approval.assert_called_once_with(
12345, "octocat/hello-world", 42, 200, "Duplicate Stampbot approval"
)
tests/test_webhook_handler.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.dismiss_approval' id='139807933697728'>
args = (12345, 'octocat/hello-world', 42, 200, 'Duplicate Stampbot approval')
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:11:53.450596Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:11:53.451138Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:11:53.451388Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:11:53.450596Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:11:53.451138Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:11:53.451388Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_and_labeled_race_leaves_one_approval - AssertionError: Expected 'dismiss_approval' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 348 passed, 3 deselected, 1 warning in 4.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -119,7 +119,7 @@
org_repo_full_name = None
if (
- owner_type == "Organization"
+ owner_type != "Organization"
and owner_login
and repo_full_name != f"{owner_login}/.github"
):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fa08397fb30>
mock_github_client = <MagicMock name='github_client' id='140327379574064'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140327379579440'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:50:52.324142Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": "octocat/.github"}, "event": "No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:50:52.324901Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:50:52.325170Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:50:52.325704Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:50:52.325952Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:50:52.324142Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': 'octocat/.github'}, 'event': 'No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:50:52.324901Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:50:52.325170Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:50:52.325704Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:50:52.325952Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -119,7 +119,7 @@
org_repo_full_name = None
if (
- owner_type == "Organization"
+ owner_type < "Organization"
and owner_login
and repo_full_name != f"{owner_login}/.github"
):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.....................................................................F
=================================== FAILURES ===================================
_____________________ test_org_github_repo_config_fallback _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f8825083e90>
mock_github_client = <MagicMock name='github_client' id='140222713263984'>
@pytest.mark.asyncio
async def test_org_github_repo_config_fallback(webhook_handler, mock_github_client):
"""Test org .github stampbot.toml is used when repo config is missing."""
payload = load_fixture("pr_labeled_autoapprove")
payload["repository"]["full_name"] = "acme/widgets"
payload["repository"]["default_branch"] = "main"
payload["repository"]["owner"] = {"login": "acme", "type": "Organization"}
payload["pull_request"]["labels"] = [{"name": "org-approve"}]
payload["label"] = {"name": "org-approve"}
def get_repo_file_side_effect(
_installation_id,
repo_full_name,
_file_path,
_ref,
*,
missing_repository_is_optional=False,
):
if repo_full_name == "acme/widgets":
assert missing_repository_is_optional is False
return None
if repo_full_name == "acme/.github":
assert missing_repository_is_optional is True
return 'approval_labels = ["org-approve"]'
return None
mock_github_client.get_repo_file.side_effect = get_repo_file_side_effect
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:1178: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:45:18.151704Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "acme/widgets", "org_repo": null}, "event": "No stampbot.toml found in acme/widgets, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:45:18.152246Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:45:18.151704Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'acme/widgets', 'org_repo': None}, 'event': 'No stampbot.toml found in acme/widgets, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:45:18.152246Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_org_github_repo_config_fallback - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 429 passed, 3 deselected, 1 warning in 5.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -119,7 +119,7 @@
org_repo_full_name = None
if (
- owner_type == "Organization"
+ owner_type <= "Organization"
and owner_login
and repo_full_name != f"{owner_login}/.github"
):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
...............F
=================================== FAILURES ===================================
____________________ test_pull_request_review_comment_event ____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb2fb140230>
mock_github_client = <MagicMock name='github_client' id='140406694052928'>
@pytest.mark.asyncio
async def test_pull_request_review_comment_event(webhook_handler, mock_github_client):
"""Test handling pull_request_review_comment event type."""
payload = {
"action": "created",
"pull_request": {
"number": 42,
},
"comment": {
"body": "@stampbot approve",
"user": {"login": "testuser"},
},
"repository": {
"full_name": "owner/repo",
},
"installation": {"id": 12345},
}
result = await webhook_handler.handle_event("pull_request_review_comment", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:1487: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request_review_comment", "action": "created"}, "event": "Received pull_request_review_comment event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:21:29.900607Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "owner/repo", "error": "'<=' not supported between instances of 'NoneType' and 'str'"}, "event": "Error loading config from owner/repo: '<=' not supported between instances of 'NoneType' and 'str', disabling automation", "logger": "stampbot.repo_policy", "level": "warning", "timestamp": "2026-09-04T21:21:29.901094Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request_review_comment', 'action': 'created'}, 'event': 'Received pull_request_review_comment event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:21:29.900607Z'}
WARNING stampbot.repo_policy:repo_policy.py:172 {'extra': {'repo': 'owner/repo', 'error': "'<=' not supported between instances of 'NoneType' and 'str'"}, 'event': "Error loading config from owner/repo: '<=' not supported between instances of 'NoneType' and 'str', disabling automation", 'logger': 'stampbot.repo_policy', 'level': 'warning', 'timestamp': '2026-09-04T21:21:29.901094Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pull_request_review_comment_event - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 447 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -119,7 +119,7 @@
org_repo_full_name = None
if (
- owner_type == "Organization"
+ owner_type > "Organization"
and owner_login
and repo_full_name != f"{owner_login}/.github"
):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fd9d062d0d0>
mock_github_client = <MagicMock name='github_client' id='140573479738672'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140573479744048'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:03:17.387627Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": "octocat/.github"}, "event": "No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:03:17.388343Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:03:17.388619Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:03:17.389115Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:03:17.389348Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:03:17.387627Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': 'octocat/.github'}, 'event': 'No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:03:17.388343Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:03:17.388619Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:03:17.389115Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:03:17.389348Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -119,7 +119,7 @@
org_repo_full_name = None
if (
- owner_type == "Organization"
+ owner_type >= "Organization"
and owner_login
and repo_full_name != f"{owner_login}/.github"
):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f776adf5e50>
mock_github_client = <MagicMock name='github_client' id='140150867238192'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140150867243568'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:24:18.459592Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": "octocat/.github"}, "event": "No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:24:18.460265Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:24:18.460536Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:24:18.461036Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:24:18.461268Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:24:18.459592Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': 'octocat/.github'}, 'event': 'No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:24:18.460265Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:24:18.460536Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:24:18.461036Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:24:18.461268Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -119,7 +119,7 @@
org_repo_full_name = None
if (
- owner_type == "Organization"
+ owner_type is "Organization"
and owner_login
and repo_full_name != f"{owner_login}/.github"
):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/repo_policy.py:122
/home/runner/work/stampbot/stampbot/stampbot/repo_policy.py:122: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
owner_type is "Organization"
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -119,7 +119,7 @@
org_repo_full_name = None
if (
- owner_type == "Organization"
+ owner_type is not "Organization"
and owner_login
and repo_full_name != f"{owner_login}/.github"
):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7ff0db4bbad0>
mock_github_client = <MagicMock name='github_client' id='140672441539888'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140672441545264'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:43:53.975450Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": "octocat/.github"}, "event": "No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:43:53.976155Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:43:53.976405Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:43:53.976920Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:43:53.977155Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:43:53.975450Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': 'octocat/.github'}, 'event': 'No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:43:53.976155Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:43:53.976405Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:43:53.976920Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:43:53.977155Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/repo_policy.py:122
/home/runner/work/stampbot/stampbot/stampbot/repo_policy.py:122: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
owner_type is not "Organization"
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 2 warnings in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Eq, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -121,7 +121,7 @@
if (
owner_type == "Organization"
and owner_login
- and repo_full_name != f"{owner_login}/.github"
+ and repo_full_name == f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"
org_content = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.....................................................................F
=================================== FAILURES ===================================
_____________________ test_org_github_repo_config_fallback _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f95a1cd04d0>
mock_github_client = <MagicMock name='github_client' id='140280636422000'>
@pytest.mark.asyncio
async def test_org_github_repo_config_fallback(webhook_handler, mock_github_client):
"""Test org .github stampbot.toml is used when repo config is missing."""
payload = load_fixture("pr_labeled_autoapprove")
payload["repository"]["full_name"] = "acme/widgets"
payload["repository"]["default_branch"] = "main"
payload["repository"]["owner"] = {"login": "acme", "type": "Organization"}
payload["pull_request"]["labels"] = [{"name": "org-approve"}]
payload["label"] = {"name": "org-approve"}
def get_repo_file_side_effect(
_installation_id,
repo_full_name,
_file_path,
_ref,
*,
missing_repository_is_optional=False,
):
if repo_full_name == "acme/widgets":
assert missing_repository_is_optional is False
return None
if repo_full_name == "acme/.github":
assert missing_repository_is_optional is True
return 'approval_labels = ["org-approve"]'
return None
mock_github_client.get_repo_file.side_effect = get_repo_file_side_effect
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:1178: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:01:32.140743Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "acme/widgets", "org_repo": null}, "event": "No stampbot.toml found in acme/widgets, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:01:32.141286Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:01:32.140743Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'acme/widgets', 'org_repo': None}, 'event': 'No stampbot.toml found in acme/widgets, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:01:32.141286Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_org_github_repo_config_fallback - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 429 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Lt, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -121,7 +121,7 @@
if (
owner_type == "Organization"
and owner_login
- and repo_full_name != f"{owner_login}/.github"
+ and repo_full_name < f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"
org_content = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.....................................................................F
=================================== FAILURES ===================================
_____________________ test_org_github_repo_config_fallback _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f33ba7cba10>
mock_github_client = <MagicMock name='github_client' id='139860143306608'>
@pytest.mark.asyncio
async def test_org_github_repo_config_fallback(webhook_handler, mock_github_client):
"""Test org .github stampbot.toml is used when repo config is missing."""
payload = load_fixture("pr_labeled_autoapprove")
payload["repository"]["full_name"] = "acme/widgets"
payload["repository"]["default_branch"] = "main"
payload["repository"]["owner"] = {"login": "acme", "type": "Organization"}
payload["pull_request"]["labels"] = [{"name": "org-approve"}]
payload["label"] = {"name": "org-approve"}
def get_repo_file_side_effect(
_installation_id,
repo_full_name,
_file_path,
_ref,
*,
missing_repository_is_optional=False,
):
if repo_full_name == "acme/widgets":
assert missing_repository_is_optional is False
return None
if repo_full_name == "acme/.github":
assert missing_repository_is_optional is True
return 'approval_labels = ["org-approve"]'
return None
mock_github_client.get_repo_file.side_effect = get_repo_file_side_effect
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:1178: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:39:12.038043Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "acme/widgets", "org_repo": null}, "event": "No stampbot.toml found in acme/widgets, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:39:12.038588Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:39:12.038043Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'acme/widgets', 'org_repo': None}, 'event': 'No stampbot.toml found in acme/widgets, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:39:12.038588Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_org_github_repo_config_fallback - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 429 passed, 3 deselected, 1 warning in 4.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_LtE, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -121,7 +121,7 @@
if (
owner_type == "Organization"
and owner_login
- and repo_full_name != f"{owner_login}/.github"
+ and repo_full_name <= f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"
org_content = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.....................................................................F
=================================== FAILURES ===================================
_____________________ test_org_github_repo_config_fallback _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f3d65ed42f0>
mock_github_client = <MagicMock name='github_client' id='139901676517232'>
@pytest.mark.asyncio
async def test_org_github_repo_config_fallback(webhook_handler, mock_github_client):
"""Test org .github stampbot.toml is used when repo config is missing."""
payload = load_fixture("pr_labeled_autoapprove")
payload["repository"]["full_name"] = "acme/widgets"
payload["repository"]["default_branch"] = "main"
payload["repository"]["owner"] = {"login": "acme", "type": "Organization"}
payload["pull_request"]["labels"] = [{"name": "org-approve"}]
payload["label"] = {"name": "org-approve"}
def get_repo_file_side_effect(
_installation_id,
repo_full_name,
_file_path,
_ref,
*,
missing_repository_is_optional=False,
):
if repo_full_name == "acme/widgets":
assert missing_repository_is_optional is False
return None
if repo_full_name == "acme/.github":
assert missing_repository_is_optional is True
return 'approval_labels = ["org-approve"]'
return None
mock_github_client.get_repo_file.side_effect = get_repo_file_side_effect
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:1178: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:17:43.468122Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "acme/widgets", "org_repo": null}, "event": "No stampbot.toml found in acme/widgets, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:17:43.468683Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:17:43.468122Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'acme/widgets', 'org_repo': None}, 'event': 'No stampbot.toml found in acme/widgets, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:17:43.468683Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_org_github_repo_config_fallback - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 429 passed, 3 deselected, 1 warning in 4.96s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_NotEq_Gt, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -121,7 +121,7 @@
if (
owner_type == "Organization"
and owner_login
- and repo_full_name != f"{owner_login}/.github"
+ and repo_full_name > f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"
org_content = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_NotEq_GtE, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -121,7 +121,7 @@
if (
owner_type == "Organization"
and owner_login
- and repo_full_name != f"{owner_login}/.github"
+ and repo_full_name >= f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"
org_content = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Is, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -121,7 +121,7 @@
if (
owner_type == "Organization"
and owner_login
- and repo_full_name != f"{owner_login}/.github"
+ and repo_full_name is f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"
org_content = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.....................................................................F
=================================== FAILURES ===================================
_____________________ test_org_github_repo_config_fallback _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1b63badb50>
mock_github_client = <MagicMock name='github_client' id='139755609503600'>
@pytest.mark.asyncio
async def test_org_github_repo_config_fallback(webhook_handler, mock_github_client):
"""Test org .github stampbot.toml is used when repo config is missing."""
payload = load_fixture("pr_labeled_autoapprove")
payload["repository"]["full_name"] = "acme/widgets"
payload["repository"]["default_branch"] = "main"
payload["repository"]["owner"] = {"login": "acme", "type": "Organization"}
payload["pull_request"]["labels"] = [{"name": "org-approve"}]
payload["label"] = {"name": "org-approve"}
def get_repo_file_side_effect(
_installation_id,
repo_full_name,
_file_path,
_ref,
*,
missing_repository_is_optional=False,
):
if repo_full_name == "acme/widgets":
assert missing_repository_is_optional is False
return None
if repo_full_name == "acme/.github":
assert missing_repository_is_optional is True
return 'approval_labels = ["org-approve"]'
return None
mock_github_client.get_repo_file.side_effect = get_repo_file_side_effect
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:1178: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:42:22.178814Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "acme/widgets", "org_repo": null}, "event": "No stampbot.toml found in acme/widgets, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:42:22.179366Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:42:22.178814Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'acme/widgets', 'org_repo': None}, 'event': 'No stampbot.toml found in acme/widgets, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:42:22.179366Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_org_github_repo_config_fallback - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 429 passed, 3 deselected, 1 warning in 5.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_NotEq_IsNot, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -121,7 +121,7 @@
if (
owner_type == "Organization"
and owner_login
- and repo_full_name != f"{owner_login}/.github"
+ and repo_full_name is not f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"
org_content = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 0, definition_name: __init__
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -42,7 +42,7 @@
cache_seconds: Seconds to keep a valid result. Zero disables caching.
"""
self._cache: TTLCache[PolicyKey, RepoConfig] | None = (
- TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > 0 else None
+ TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds == 0 else None
)
async def get(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f18ec543290>
mock_github_client = <MagicMock name='github_client' id='139745020574896'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='139745020563808'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:39:43.807029Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:39:43.807628Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:39:43.807880Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:39:43.808370Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:39:43.808698Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:39:43.808921Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:39:43.807029Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:39:43.807628Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:39:43.807880Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:39:43.808370Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:39:43.808698Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:39:43.808921Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.16s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 0, definition_name: __init__
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -42,7 +42,7 @@
cache_seconds: Seconds to keep a valid result. Zero disables caching.
"""
self._cache: TTLCache[PolicyKey, RepoConfig] | None = (
- TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > 0 else None
+ TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds != 0 else None
)
async def get(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 0, definition_name: __init__
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -42,7 +42,7 @@
cache_seconds: Seconds to keep a valid result. Zero disables caching.
"""
self._cache: TTLCache[PolicyKey, RepoConfig] | None = (
- TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > 0 else None
+ TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds < 0 else None
)
async def get(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f4f1bf7f2f0>
mock_github_client = <MagicMock name='github_client' id='139977745956016'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='139977745944928'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:54:57.714426Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:54:57.715033Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:54:57.715289Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:54:57.715805Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:54:57.716143Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:54:57.716360Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:54:57.714426Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:54:57.715033Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:54:57.715289Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:54:57.715805Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:54:57.716143Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:54:57.716360Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.27s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 0, definition_name: __init__
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -42,7 +42,7 @@
cache_seconds: Seconds to keep a valid result. Zero disables caching.
"""
self._cache: TTLCache[PolicyKey, RepoConfig] | None = (
- TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > 0 else None
+ TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds <= 0 else None
)
async def get(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6fc5fbf290>
mock_github_client = <MagicMock name='github_client' id='140118036790448'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140118036779360'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:52:03.535254Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:52:03.535887Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:52:03.536143Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:52:03.536663Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:52:03.536973Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:52:03.537193Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:52:03.535254Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:52:03.535887Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:52:03.536143Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:52:03.536663Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:52:03.536973Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:52:03.537193Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.22s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 0, definition_name: __init__
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -42,7 +42,7 @@
cache_seconds: Seconds to keep a valid result. Zero disables caching.
"""
self._cache: TTLCache[PolicyKey, RepoConfig] | None = (
- TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > 0 else None
+ TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds >= 0 else None
)
async def get(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 0, definition_name: get
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -76,7 +76,7 @@
installation_id, repo_full_name, default_branch, owner_login, owner_type
)
- if self._cache is not None and repo_config.config_error is None:
+ if self._cache is not None and repo_config.config_error is not None:
self._cache[key] = repo_config
return repo_config........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fca00145d90>
mock_github_client = <MagicMock name='github_client' id='140505561325856'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140505561321488'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:50:05.939968Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:50:05.940600Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:50:05.940864Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:50:05.941361Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:50:05.941695Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:50:05.941921Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:50:05.939968Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:50:05.940600Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:50:05.940864Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:50:05.941361Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:50:05.941695Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:50:05.941921Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 0, definition_name: get
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -66,7 +66,7 @@
RepoConfig instance. ``config_error`` is set when automation must stop.
"""
key: PolicyKey = (installation_id, repo_full_name, default_branch)
- if self._cache is not None:
+ if self._cache is None:
cached = self._cache.get(key)
if cached is not None:
repo_config_loads_total.labels(status="cached").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f83c315daf0>
mock_github_client = <MagicMock name='github_client' id='140203890647328'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140203890642960'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:56:48.079753Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:56:48.080358Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:56:48.080661Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:56:48.081160Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:56:48.081475Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:56:48.081711Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:56:48.079753Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:56:48.080358Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:56:48.080661Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:56:48.081160Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:56:48.081475Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:56:48.081711Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 1, definition_name: get
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -68,7 +68,7 @@
key: PolicyKey = (installation_id, repo_full_name, default_branch)
if self._cache is not None:
cached = self._cache.get(key)
- if cached is not None:
+ if cached is None:
repo_config_loads_total.labels(status="cached").inc()
return cached
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1449dfb5c0>
mock_github_client = <MagicMock name='github_client' id='139725108803104'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f1449dfb5c0>
payload = {'action': 'opened', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pulls/42'...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
> if repo_config.config_error:
^^^^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'config_error'
stampbot/webhook_handler.py:232: AttributeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:27:12.195108Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:27:12.195108Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AttributeError: 'NoneType' object has no attribute 'config_error'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 2, definition_name: get
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -76,7 +76,7 @@
installation_id, repo_full_name, default_branch, owner_login, owner_type
)
- if self._cache is not None and repo_config.config_error is None:
+ if self._cache is None and repo_config.config_error is None:
self._cache[key] = repo_config
return repo_config........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7faebb67cb90>
mock_github_client = <MagicMock name='github_client' id='140388450836768'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140388450832400'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:59:14.248995Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:59:14.249585Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:59:14.249835Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:59:14.250323Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:59:14.250645Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:59:14.250865Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:59:14.248995Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:59:14.249585Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:59:14.249835Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:59:14.250323Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:59:14.250645Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:59:14.250865Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 0, definition_name: __init__
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -42,7 +42,7 @@
cache_seconds: Seconds to keep a valid result. Zero disables caching.
"""
self._cache: TTLCache[PolicyKey, RepoConfig] | None = (
- TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > 0 else None
+ TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if not cache_seconds > 0 else None
)
async def get(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f61ed6fbcb0>
mock_github_client = <MagicMock name='github_client' id='140058569178288'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140058569167200'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:26:34.934966Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:26:34.935594Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:26:34.935851Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:26:34.936348Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:26:34.936693Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:26:34.936917Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:26:34.934966Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:26:34.935594Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:26:34.935851Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:26:34.936348Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:26:34.936693Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:26:34.936917Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 1, definition_name: get
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -66,7 +66,7 @@
RepoConfig instance. ``config_error`` is set when automation must stop.
"""
key: PolicyKey = (installation_id, repo_full_name, default_branch)
- if self._cache is not None:
+ if not self._cache is not None:
cached = self._cache.get(key)
if cached is not None:
repo_config_loads_total.labels(status="cached").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f5a0820b950>
mock_github_client = <MagicMock name='github_client' id='140024659583264'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140024659578896'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:28:00.826686Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:28:00.827243Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:28:00.827516Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:28:00.828009Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T20:28:00.828317Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:28:00.828575Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:28:00.826686Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:28:00.827243Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:28:00.827516Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:28:00.828009Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T20:28:00.828317Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:28:00.828575Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 2, definition_name: get
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -68,7 +68,7 @@
key: PolicyKey = (installation_id, repo_full_name, default_branch)
if self._cache is not None:
cached = self._cache.get(key)
- if cached is not None:
+ if not cached is not None:
repo_config_loads_total.labels(status="cached").inc()
return cached
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.........................................................F
=================================== FAILURES ===================================
____________________ test_pr_opened_with_autoapprove_label _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f287b4275c0>
mock_github_client = <MagicMock name='github_client' id='139811838686752'>
@pytest.mark.asyncio
async def test_pr_opened_with_autoapprove_label(webhook_handler, mock_github_client):
"""Test PR opened with autoapprove label triggers approval."""
payload = load_fixture("pr_opened_with_autoapprove_label")
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.webhook_handler.WebhookHandler object at 0x7f287b4275c0>
payload = {'action': 'opened', 'number': 42, 'pull_request': {'url': 'https://api.github.com/repos/octocat/hello-world/pulls/42'...ository': {'id': 12345678, 'node_id': 'R_kgtest', 'name': 'hello-world', 'full_name': 'octocat/hello-world', ...}, ...}
async def _handle_pull_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle pull request events.
Args:
payload: Event payload
Returns:
Response dictionary
"""
action = payload.get("action")
pr = payload.get("pull_request", {})
pr_number = pr.get("number")
repo = payload.get("repository", {})
repo_full_name = repo.get("full_name")
repo_default_branch = repo.get("default_branch") or "main"
repo_owner = repo.get("owner", {})
owner_login = repo_owner.get("login")
owner_type = repo_owner.get("type")
installation_id = payload.get("installation", {}).get("id")
with create_span(
"webhook.handle_pull_request",
{
"github.repo": repo_full_name or "unknown",
"github.pr_number": pr_number or 0,
"github.action": action or "unknown",
},
) as span:
if not all([pr_number, repo_full_name, installation_id]):
logger.warning("Missing required fields in PR event")
add_span_attributes(span, {"webhook.result": "missing_fields"})
set_span_ok(span)
return {"status": "error", "message": "Missing required fields"}
if action not in PULL_REQUEST_ACTIONS:
# Edits, review requests, closes, and similar actions never change
# review state, so they must not cost a policy read.
add_span_attributes(span, {"webhook.result": "action_ignored"})
set_span_ok(span)
return {"status": "ignored", "message": f"Action {action} not handled"}
repo_config = await self._policy.get(
installation_id,
repo_full_name,
repo_default_branch,
owner_login,
owner_type,
)
> if repo_config.config_error:
^^^^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'config_error'
stampbot/webhook_handler.py:232: AttributeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:57:39.031849Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:57:39.031849Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_opened_with_autoapprove_label - AttributeError: 'NoneType' object has no attribute 'config_error'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 345 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 3, definition_name: get
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -76,7 +76,7 @@
installation_id, repo_full_name, default_branch, owner_login, owner_type
)
- if self._cache is not None and repo_config.config_error is None:
+ if not self._cache is not None and repo_config.config_error is None:
self._cache[key] = repo_config
return repo_config........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fb219432b70>
mock_github_client = <MagicMock name='github_client' id='140402901999904'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140402901995536'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:48:04.965593Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:48:04.966238Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:48:04.966506Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:48:04.967016Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:48:04.967325Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:48:04.967559Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:48:04.965593Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:48:04.966238Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:48:04.966506Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:48:04.967016Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:48:04.967325Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:48:04.967559Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/hello-world', 'stampbot.toml', 'main')].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 4, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -114,7 +114,7 @@
default_branch,
)
- if content:
+ if not content:
return self._parse(span, content, source_repo=repo_full_name)
org_repo_full_name = None........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f6b597228a0>
mock_github_client = <MagicMock name='github_client' id='140098949226784'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:48:11.547526Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:48:11.548113Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:48:11.547526Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:48:11.548113Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 5, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -118,7 +118,7 @@
return self._parse(span, content, source_repo=repo_full_name)
org_repo_full_name = None
- if (
+ if not (
owner_type == "Organization"
and owner_login
and repo_full_name != f"{owner_login}/.github"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f0cd2e57770>
mock_github_client = <MagicMock name='github_client' id='139693052222768'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='139693052228144'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:55:49.787200Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": "octocat/.github"}, "event": "No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:55:49.787928Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:55:49.788187Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:55:49.788708Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:55:49.788952Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:55:49.787200Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': 'octocat/.github'}, 'event': 'No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:55:49.787928Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:55:49.788187Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:55:49.788708Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:55:49.788952Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 6, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -132,7 +132,7 @@
None,
missing_repository_is_optional=True,
)
- if org_content:
+ if not org_content:
return self._parse(span, org_content, source_repo=org_repo_full_name)
repo_config = RepoConfig.default_or_config_error()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.....................................................................F
=================================== FAILURES ===================================
_____________________ test_org_github_repo_config_fallback _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fd8aba482f0>
mock_github_client = <MagicMock name='github_client' id='140568567609200'>
@pytest.mark.asyncio
async def test_org_github_repo_config_fallback(webhook_handler, mock_github_client):
"""Test org .github stampbot.toml is used when repo config is missing."""
payload = load_fixture("pr_labeled_autoapprove")
payload["repository"]["full_name"] = "acme/widgets"
payload["repository"]["default_branch"] = "main"
payload["repository"]["owner"] = {"login": "acme", "type": "Organization"}
payload["pull_request"]["labels"] = [{"name": "org-approve"}]
payload["label"] = {"name": "org-approve"}
def get_repo_file_side_effect(
_installation_id,
repo_full_name,
_file_path,
_ref,
*,
missing_repository_is_optional=False,
):
if repo_full_name == "acme/widgets":
assert missing_repository_is_optional is False
return None
if repo_full_name == "acme/.github":
assert missing_repository_is_optional is True
return 'approval_labels = ["org-approve"]'
return None
mock_github_client.get_repo_file.side_effect = get_repo_file_side_effect
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'ignored' == 'success'
E
E - success
E + ignored
tests/test_webhook_handler.py:1178: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:45:04.287590Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "acme/widgets", "org_repo": "acme/.github"}, "event": "No stampbot.toml found in acme/widgets or acme/.github, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T21:45:04.288269Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:45:04.287590Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'acme/widgets', 'org_repo': 'acme/.github'}, 'event': 'No stampbot.toml found in acme/widgets or acme/.github, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T21:45:04.288269Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_org_github_repo_config_fallback - AssertionError: assert 'ignored' == 'success'
- success
+ ignored
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 429 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 7, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -136,7 +136,7 @@
return self._parse(span, org_content, source_repo=org_repo_full_name)
repo_config = RepoConfig.default_or_config_error()
- if repo_config.config_error:
+ if not repo_config.config_error:
error = ValueError(repo_config.config_error)
repo_config_loads_total.labels(status="error").inc()
logger.warning(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.14s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 8, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -160,7 +160,7 @@
logger.info(
"No stampbot.toml found in %s%s, using defaults",
repo_full_name,
- f" or {org_repo_full_name}" if org_repo_full_name else "",
+ f" or {org_repo_full_name}" if not org_repo_full_name else "",
extra={"repo": repo_full_name, "org_repo": org_repo_full_name},
)
add_span_attributes(span, {"config.result": "default"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -130,7 +130,7 @@
org_repo_full_name,
POLICY_FILE,
None,
- missing_repository_is_optional=True,
+ missing_repository_is_optional=False,
)
if org_content:
return self._parse(span, org_content, source_repo=org_repo_full_name)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.....................................................................F
=================================== FAILURES ===================================
_____________________ test_org_github_repo_config_fallback _____________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f09c7c2c9b0>
mock_github_client = <MagicMock name='github_client' id='139679983466352'>
@pytest.mark.asyncio
async def test_org_github_repo_config_fallback(webhook_handler, mock_github_client):
"""Test org .github stampbot.toml is used when repo config is missing."""
payload = load_fixture("pr_labeled_autoapprove")
payload["repository"]["full_name"] = "acme/widgets"
payload["repository"]["default_branch"] = "main"
payload["repository"]["owner"] = {"login": "acme", "type": "Organization"}
payload["pull_request"]["labels"] = [{"name": "org-approve"}]
payload["label"] = {"name": "org-approve"}
def get_repo_file_side_effect(
_installation_id,
repo_full_name,
_file_path,
_ref,
*,
missing_repository_is_optional=False,
):
if repo_full_name == "acme/widgets":
assert missing_repository_is_optional is False
return None
if repo_full_name == "acme/.github":
assert missing_repository_is_optional is True
return 'approval_labels = ["org-approve"]'
return None
mock_github_client.get_repo_file.side_effect = get_repo_file_side_effect
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:1178: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:44:08.593226Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "acme/widgets", "error": "assert False is True"}, "event": "Error loading config from acme/widgets: assert False is True, disabling automation", "logger": "stampbot.repo_policy", "level": "warning", "timestamp": "2026-09-04T21:44:08.593952Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "warning"}
{"extra": {"repo": "acme/widgets", "error": "Unable to load Stampbot configuration; automation is disabled"}, "event": "Invalid Stampbot configuration for acme/widgets: Unable to load Stampbot configuration; automation is disabled", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:44:08.594096Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:44:08.593226Z'}
WARNING stampbot.repo_policy:repo_policy.py:172 {'extra': {'repo': 'acme/widgets', 'error': 'assert False is True'}, 'event': 'Error loading config from acme/widgets: assert False is True, disabling automation', 'logger': 'stampbot.repo_policy', 'level': 'warning', 'timestamp': '2026-09-04T21:44:08.593952Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'acme/widgets', 'error': 'Unable to load Stampbot configuration; automation is disabled'}, 'event': 'Invalid Stampbot configuration for acme/widgets: Unable to load Stampbot configuration; automation is disabled', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:44:08.594096Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_org_github_repo_config_fallback - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 429 passed, 3 deselected, 1 warning in 5.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 0, definition_name: get
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -76,7 +76,7 @@
installation_id, repo_full_name, default_branch, owner_login, owner_type
)
- if self._cache is not None and repo_config.config_error is None:
+ if self._cache is not None or repo_config.config_error is None:
self._cache[key] = repo_config
return repo_config........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.............F
=================================== FAILURES ===================================
_______________________ test_repo_config_cache_disabled ________________________
mock_github_client = <MagicMock name='github_client' id='140098544228336'>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f6b40140440>
@pytest.mark.asyncio
async def test_repo_config_cache_disabled(mock_github_client, monkeypatch):
"""Test a zero cache lifetime reads policy on every event."""
monkeypatch.setattr("stampbot.webhook_handler.repo_config_cache_seconds", lambda: 0)
from stampbot.webhook_handler import WebhookHandler
handler = WebhookHandler()
payload = load_fixture("pr_labeled_autoapprove")
> await handler.handle_event("pull_request", payload)
tests/test_webhook_handler.py:399:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:224: in _handle_pull_request
repo_config = await self._policy.get(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.repo_policy.RepoPolicyResolver object at 0x7f6b3bd51cc0>
installation_id = 12345, repo_full_name = 'octocat/hello-world'
default_branch = 'main', owner_login = 'octocat', owner_type = 'User'
async def get(
self,
installation_id: int,
repo_full_name: str,
default_branch: str,
owner_login: str | None,
owner_type: str | None,
) -> RepoConfig:
"""Return the effective policy for a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
default_branch: Repository default branch
owner_login: Repository owner login
owner_type: Repository owner type (Organization/User)
Returns:
RepoConfig instance. ``config_error`` is set when automation must stop.
"""
key: PolicyKey = (installation_id, repo_full_name, default_branch)
if self._cache is not None:
cached = self._cache.get(key)
if cached is not None:
repo_config_loads_total.labels(status="cached").inc()
return cached
repo_config = await self._load(
installation_id, repo_full_name, default_branch, owner_login, owner_type
)
if self._cache is not None or repo_config.config_error is None:
> self._cache[key] = repo_config
^^^^^^^^^^^^^^^^
E TypeError: 'NoneType' object does not support item assignment
stampbot/repo_policy.py:80: TypeError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:46.956696Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": null}, "event": "No stampbot.toml found in octocat/hello-world, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:49:46.957217Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:46.956696Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': None}, 'event': 'No stampbot.toml found in octocat/hello-world, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:49:46.957217Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_cache_disabled - TypeError: 'NoneType' object does not support item assignment
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 373 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 1, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -120,7 +120,7 @@
org_repo_full_name = None
if (
owner_type == "Organization"
- and owner_login
+ or owner_login
and repo_full_name != f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f353db0f770>
mock_github_client = <MagicMock name='github_client' id='139866643225904'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='139866643231280'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:05.826204Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": "octocat/.github"}, "event": "No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:20:05.826892Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:05.827144Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:05.827648Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:20:05.827885Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:05.826204Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': 'octocat/.github'}, 'event': 'No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:20:05.826892Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:05.827144Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:05.827648Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:20:05.827885Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 2, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -121,7 +121,7 @@
if (
owner_type == "Organization"
and owner_login
- and repo_full_name != f"{owner_login}/.github"
+ or repo_full_name != f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"
org_content = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
............F
=================================== FAILURES ===================================
__________________ test_repo_config_is_cached_between_events ___________________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7fcb7e8b6ff0>
mock_github_client = <MagicMock name='github_client' id='140511977086256'>
@pytest.mark.asyncio
async def test_repo_config_is_cached_between_events(webhook_handler, mock_github_client):
"""Test a second event for the same repository reuses the parsed policy."""
payload = load_fixture("pr_labeled_autoapprove")
first = await webhook_handler.handle_event("pull_request", payload)
second = await webhook_handler.handle_event("pull_request", payload)
assert first["status"] == "success"
assert second["status"] == "success"
> mock_github_client.get_repo_file.assert_called_once()
tests/test_webhook_handler.py:386:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='140511977091632'>
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
E Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
E call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:965: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:56:04.786648Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "org_repo": "octocat/.github"}, "event": "No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults", "logger": "stampbot.repo_policy", "level": "info", "timestamp": "2026-09-04T22:56:04.787490Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:56:04.787768Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:56:04.788266Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "pr_number": 42, "label": "autoapprove"}, "event": "PR #42 has approval label: autoapprove", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:56:04.788529Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:56:04.786648Z'}
INFO stampbot.repo_policy:repo_policy.py:160 {'extra': {'repo': 'octocat/hello-world', 'org_repo': 'octocat/.github'}, 'event': 'No stampbot.toml found in octocat/hello-world or octocat/.github, using defaults', 'logger': 'stampbot.repo_policy', 'level': 'info', 'timestamp': '2026-09-04T22:56:04.787490Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:56:04.787768Z'}
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:56:04.788266Z'}
INFO stampbot.webhook_handler:webhook_handler.py:442 {'extra': {'repo': 'octocat/hello-world', 'pr_number': 42, 'label': 'autoapprove'}, 'event': 'PR #42 has approval label: autoapprove', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:56:04.788529Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_repo_config_is_cached_between_events - AssertionError: Expected 'get_repo_file' to have been called once. Called 2 times.
Calls: [call(12345, 'octocat/hello-world', 'stampbot.toml', 'main'),
call(12345, 'octocat/.github', 'stampbot.toml', None, missing_repository_is_optional=True)].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 372 passed, 3 deselected, 1 warning in 4.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -103,7 +103,7 @@
"""
with create_span(
"webhook.get_repo_config",
- {"github.repo": repo_full_name, "github.ref": default_branch or "default"},
+ {"github.repo": repo_full_name, "github.ref": default_branch and "default"},
) as span:
try:
content = await run_in_threadpool(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.27s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 0, definition_name: _load
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -167,7 +167,7 @@
set_span_ok(span)
return repo_config
- except Exception as e:
+ except CosmicRayTestingException as e:
repo_config_loads_total.labels(status="error").inc()
logger.warning(
"Error loading config from %s: %s, disabling automation",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
.......................................................................F [ 93%]
=================================== FAILURES ===================================
___________________ test_target_repo_not_found_fails_closed ____________________
self = <stampbot.repo_policy.RepoPolicyResolver object at 0x7f19885bc500>
installation_id = 12345, repo_full_name = 'octocat/hello-world'
default_branch = 'main', owner_login = 'octocat', owner_type = 'User'
async def _load(
self,
installation_id: int,
repo_full_name: str,
default_branch: str,
owner_login: str | None,
owner_type: str | None,
) -> RepoConfig:
"""Read policy from GitHub without consulting the cache.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
default_branch: Repository default branch
owner_login: Repository owner login
owner_type: Repository owner type (Organization/User)
Returns:
RepoConfig instance (default if file not found)
"""
with create_span(
"webhook.get_repo_config",
{"github.repo": repo_full_name, "github.ref": default_branch or "default"},
) as span:
try:
> content = await run_in_threadpool(
github_client.get_repo_file,
installation_id,
repo_full_name,
POLICY_FILE,
default_branch,
)
stampbot/repo_policy.py:109:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/concurrency.py:34: in run_in_threadpool
return await anyio.to_thread.run_sync(func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/to_thread.py:63: in run_sync
return await get_async_backend().run_sync_in_worker_thread(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:2596: in run_sync_in_worker_thread
return await future
^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1029: in run
result = context.run(func, *args)
^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='github_client.get_repo_file' id='139747638242208'>
args = (12345, 'octocat/hello-world', 'stampbot.toml', 'main'), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f1983fbacf0>
mock_github_client = <MagicMock name='github_client' id='139747638239856'>
@pytest.mark.asyncio
async def test_target_repo_not_found_fails_closed(webhook_handler, mock_github_client):
"""Test a target-repository 404 never advances to service defaults."""
payload = load_fixture("pr_labeled_autoapprove")
mock_github_client.get_repo_file.side_effect = GithubException(
404,
{"message": "Not Found"},
None,
)
> result = await webhook_handler.handle_event("pull_request", payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_webhook_handler.py:1228:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:224: in _handle_pull_request
repo_config = await self._policy.get(
stampbot/repo_policy.py:75: in get
repo_config = await self._load(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.repo_policy.RepoPolicyResolver object at 0x7f19885bc500>
installation_id = 12345, repo_full_name = 'octocat/hello-world'
default_branch = 'main', owner_login = 'octocat', owner_type = 'User'
async def _load(
self,
installation_id: int,
repo_full_name: str,
default_branch: str,
owner_login: str | None,
owner_type: str | None,
) -> RepoConfig:
"""Read policy from GitHub without consulting the cache.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name
default_branch: Repository default branch
owner_login: Repository owner login
owner_type: Repository owner type (Organization/User)
Returns:
RepoConfig instance (default if file not found)
"""
with create_span(
"webhook.get_repo_config",
{"github.repo": repo_full_name, "github.ref": default_branch or "default"},
) as span:
try:
content = await run_in_threadpool(
github_client.get_repo_file,
installation_id,
repo_full_name,
POLICY_FILE,
default_branch,
)
if content:
return self._parse(span, content, source_repo=repo_full_name)
org_repo_full_name = None
if (
owner_type == "Organization"
and owner_login
and repo_full_name != f"{owner_login}/.github"
):
org_repo_full_name = f"{owner_login}/.github"
org_content = await run_in_threadpool(
github_client.get_repo_file,
installation_id,
org_repo_full_name,
POLICY_FILE,
None,
missing_repository_is_optional=True,
)
if org_content:
return self._parse(span, org_content, source_repo=org_repo_full_name)
repo_config = RepoConfig.default_or_config_error()
if repo_config.config_error:
error = ValueError(repo_config.config_error)
repo_config_loads_total.labels(status="error").inc()
logger.warning(
"Invalid service default configuration for %s: %s",
repo_full_name,
repo_config.config_error,
extra={"repo": repo_full_name, "error": repo_config.config_error},
)
add_span_attributes(
span,
{
"config.result": "error",
"config.error": repo_config.config_error,
"config.source_repo": "service_defaults",
},
)
set_span_error(span, error)
return repo_config
repo_config_loads_total.labels(status="default").inc()
logger.info(
"No stampbot.toml found in %s%s, using defaults",
repo_full_name,
f" or {org_repo_full_name}" if org_repo_full_name else "",
extra={"repo": repo_full_name, "org_repo": org_repo_full_name},
)
add_span_attributes(span, {"config.result": "default"})
set_span_ok(span)
return repo_config
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/repo_policy.py:170: NameError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T20:33:32.759923Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T20:33:32.759923Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_target_repo_not_found_fails_closed - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 431 passed, 3 deselected, 1 warning in 5.46s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 1, definition_name: _parse
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -195,7 +195,7 @@
"""
try:
repo_config = RepoConfig.from_toml(toml_content)
- except ValueError as e:
+ except CosmicRayTestingException as e:
repo_config_loads_total.labels(status="error").inc()
add_span_attributes(
span,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
.........F
=================================== FAILURES ===================================
_____ test_invalid_repo_and_service_configs_use_builtin_fail_closed_config _____
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f915cfd5130>
mock_github_client = <MagicMock name='github_client' id='140262302235824'>
@pytest.mark.asyncio
async def test_invalid_repo_and_service_configs_use_builtin_fail_closed_config(
webhook_handler, mock_github_client
):
"""Test repo errors do not re-read invalid service defaults for fallback."""
payload = load_fixture("pr_opened_with_autoapprove_label")
mock_github_client.get_repo_file.return_value = 'chatops_required_permission = "invalid"'
service_defaults = MagicMock(spec=["approve_commands"])
service_defaults.approve_commands = "approve"
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
result = await webhook_handler.handle_event("pull_request", payload)
assert result == {"status": "error", "message": "Invalid repository configuration"}
review_body = mock_github_client.create_pr_review_comment.call_args.args[3]
> assert "Invalid chatops_required_permission" in review_body
E AssertionError: assert 'Invalid chatops_required_permission' in 'Stampbot configuration error:\n\nUnable to load Stampbot configuration; automation is disabled\n\nPlease fix the configuration to re-enable automation.'
tests/test_webhook_handler.py:1397: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "opened"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T21:10:06.296897Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": "name 'CosmicRayTestingException' is not defined"}, "event": "Error loading config from octocat/hello-world: name 'CosmicRayTestingException' is not defined, disabling automation", "logger": "stampbot.repo_policy", "level": "warning", "timestamp": "2026-09-04T21:10:06.297437Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "warning"}
{"extra": {"repo": "octocat/hello-world", "error": "Unable to load Stampbot configuration; automation is disabled"}, "event": "Invalid Stampbot configuration for octocat/hello-world: Unable to load Stampbot configuration; automation is disabled", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T21:10:06.297603Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'opened'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T21:10:06.296897Z'}
WARNING stampbot.repo_policy:repo_policy.py:172 {'extra': {'repo': 'octocat/hello-world', 'error': "name 'CosmicRayTestingException' is not defined"}, 'event': "Error loading config from octocat/hello-world: name 'CosmicRayTestingException' is not defined, disabling automation", 'logger': 'stampbot.repo_policy', 'level': 'warning', 'timestamp': '2026-09-04T21:10:06.297437Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': 'Unable to load Stampbot configuration; automation is disabled'}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: Unable to load Stampbot configuration; automation is disabled', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T21:10:06.297603Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_invalid_repo_and_service_configs_use_builtin_fail_closed_config - AssertionError: assert 'Invalid chatops_required_permission' in 'Stampbot configuration error:\n\nUnable to load Stampbot configuration; automation is disabled\n\nPlease fix the configuration to re-enable automation.'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 441 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/repo_policy.py +++ b/stampbot/repo_policy.py @@ -17,7 +17,7 @@ POLICY_FILE = "stampbot.toml" # Upper bound on repositories whose policy one replica keeps in memory. -POLICY_CACHE_SIZE = 1024 +POLICY_CACHE_SIZE = 1025 PolicyKey = tuple[int, str, str]
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.15s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 1, definition_name: None
--- mutation diff --- --- a/stampbot/repo_policy.py +++ b/stampbot/repo_policy.py @@ -17,7 +17,7 @@ POLICY_FILE = "stampbot.toml" # Upper bound on repositories whose policy one replica keeps in memory. -POLICY_CACHE_SIZE = 1024 +POLICY_CACHE_SIZE = 1023 PolicyKey = tuple[int, str, str]
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 2, definition_name: __init__
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -42,7 +42,7 @@
cache_seconds: Seconds to keep a valid result. Zero disables caching.
"""
self._cache: TTLCache[PolicyKey, RepoConfig] | None = (
- TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > 0 else None
+ TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > 1 else None
)
async def get(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 3, definition_name: __init__
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -42,7 +42,7 @@
cache_seconds: Seconds to keep a valid result. Zero disables caching.
"""
self._cache: TTLCache[PolicyKey, RepoConfig] | None = (
- TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > 0 else None
+ TTLCache(maxsize=POLICY_CACHE_SIZE, ttl=cache_seconds) if cache_seconds > -1 else None
)
async def get(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.18s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 0, definition_name: RepoPolicyResolver
--- mutation diff ---
--- a/stampbot/repo_policy.py
+++ b/stampbot/repo_policy.py
@@ -180,8 +180,6 @@
return RepoConfig.fail_closed(
"Unable to load Stampbot configuration; automation is disabled"
)
-
- @staticmethod
def _parse(span: Span | None, toml_content: str, source_repo: str) -> RepoConfig:
"""Parse policy content with metrics and tracing.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................................F
=================================== FAILURES ===================================
___________ test_pr_synchronize_unknown_review_state_still_approves ____________
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f878c613a70>
mock_github_client = <MagicMock name='github_client' id='140220095395696'>
@pytest.mark.asyncio
async def test_pr_synchronize_unknown_review_state_still_approves(
webhook_handler, mock_github_client
):
"""Test a failed listing during a refresh approves instead of skipping."""
payload = load_fixture("pr_labeled_autoapprove")
payload["action"] = "synchronize"
payload.pop("label", None)
mock_github_client.get_repo_file.return_value = "reapprove = true"
mock_github_client.find_bot_approval_reviews.return_value = None
result = await webhook_handler.handle_event("pull_request", payload)
> assert result["status"] == "success"
E AssertionError: assert 'error' == 'success'
E
E - success
E + error
tests/test_webhook_handler.py:198: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "synchronize"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:49:59.639586Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
{"extra": {"repo": "octocat/hello-world", "error": "RepoPolicyResolver._parse() got multiple values for argument 'source_repo'"}, "event": "Error loading config from octocat/hello-world: RepoPolicyResolver._parse() got multiple values for argument 'source_repo', disabling automation", "logger": "stampbot.repo_policy", "level": "warning", "timestamp": "2026-09-04T22:49:59.640064Z", "_logger": "<_FixedFindCallerLogger stampbot.repo_policy (INFO)>", "_name": "warning"}
{"extra": {"repo": "octocat/hello-world", "error": "Unable to load Stampbot configuration; automation is disabled"}, "event": "Invalid Stampbot configuration for octocat/hello-world: Unable to load Stampbot configuration; automation is disabled", "logger": "stampbot.webhook_handler", "level": "warning", "timestamp": "2026-09-04T22:49:59.640199Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'synchronize'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:49:59.639586Z'}
WARNING stampbot.repo_policy:repo_policy.py:172 {'extra': {'repo': 'octocat/hello-world', 'error': "RepoPolicyResolver._parse() got multiple values for argument 'source_repo'"}, 'event': "Error loading config from octocat/hello-world: RepoPolicyResolver._parse() got multiple values for argument 'source_repo', disabling automation", 'logger': 'stampbot.repo_policy', 'level': 'warning', 'timestamp': '2026-09-04T22:49:59.640064Z'}
WARNING stampbot.webhook_handler:webhook_handler.py:233 {'extra': {'repo': 'octocat/hello-world', 'error': 'Unable to load Stampbot configuration; automation is disabled'}, 'event': 'Invalid Stampbot configuration for octocat/hello-world: Unable to load Stampbot configuration; automation is disabled', 'logger': 'stampbot.webhook_handler', 'level': 'warning', 'timestamp': '2026-09-04T22:49:59.640199Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pr_synchronize_unknown_review_state_still_approves - AssertionError: assert 'error' == 'success'
- success
+ error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 352 passed, 3 deselected, 1 warning in 4.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Add_Sub, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE - 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Sub, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now - BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f45b525cb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
# The App slug is read once and reused for later review scans, and
# each operation builds exactly one client on the shared credentials.
client.find_bot_approval_reviews(123456, "owner/repo", 43)
> mock_integration.get_app.assert_called_once_with()
tests/test_github_client.py:1489:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='GithubIntegration().get_app' id='139937313691632'>, args = ()
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_app' to be called once. Called 2 times.
E Calls: [call(), call()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: Expected 'get_app' to be called once. Called 2 times.
Calls: [call(), call()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.91s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Add_Mul, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE * 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Add_Mul, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now * BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Div, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE / 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f8c50106780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-330/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f8c4d042300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
> pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE / 1)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: argument should be integer or None, not 'float'
stampbot/github_client.py:241: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - TypeError: argument should be integer or None, not 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.81s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Div, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now / BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fa5dfe20b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
# The App slug is read once and reused for later review scans, and
# each operation builds exactly one client on the shared credentials.
client.find_bot_approval_reviews(123456, "owner/repo", 43)
> mock_integration.get_app.assert_called_once_with()
tests/test_github_client.py:1489:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='GithubIntegration().get_app' id='140350345189360'>, args = ()
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_app' to be called once. Called 2 times.
E Calls: [call(), call()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: Expected 'get_app' to be called once. Called 2 times.
Calls: [call(), call()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.96s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Add_FloorDiv, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE // 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_FloorDiv, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now // BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f74201e8b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
# The App slug is read once and reused for later review scans, and
# each operation builds exactly one client on the shared credentials.
client.find_bot_approval_reviews(123456, "owner/repo", 43)
> mock_integration.get_app.assert_called_once_with()
tests/test_github_client.py:1489:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='GithubIntegration().get_app' id='140136674646000'>, args = ()
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_app' to be called once. Called 2 times.
E Calls: [call(), call()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: Expected 'get_app' to be called once. Called 2 times.
Calls: [call(), call()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mod, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE % 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f0358792780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-1311/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f03556ba300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE % 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Mod, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now % BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f9306680b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
# The App slug is read once and reused for later review scans, and
# each operation builds exactly one client on the shared credentials.
client.find_bot_approval_reviews(123456, "owner/repo", 43)
> mock_integration.get_app.assert_called_once_with()
tests/test_github_client.py:1489:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='GithubIntegration().get_app' id='140269388863472'>, args = ()
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_app' to be called once. Called 2 times.
E Calls: [call(), call()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: Expected 'get_app' to be called once. Called 2 times.
Calls: [call(), call()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.90s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Add_Pow, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE ** 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_Pow, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now ** BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fc5dd244b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:38:15.360128Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: (34, 'Numerical result out of range')[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: (34, 'Numerical result out of range')", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:38:15.360128Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Add_RShift, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE >> 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_RShift, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now >> BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fb1401ecb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:06:40.428181Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'int'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'int'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'int'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'int'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:06:40.428181Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 3.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Add_LShift, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE << 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_LShift, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now << BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f975b9e8b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T23:00:16.072748Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'int'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'int'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'int'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'int'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T23:00:16.072748Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.97s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Add_BitOr, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE | 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitOr, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now | BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f5506eccb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:32:57.767217Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'int'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'int'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'int'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'int'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:32:57.767217Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitAnd, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE & 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f5b69592780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-66/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f5b664b6300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE & 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitAnd, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now & BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fb58b77cb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:34:20.972887Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'int'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'int'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'int'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'int'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:34:20.972887Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Add_BitXor, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE ^ 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Add_BitXor, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -397,7 +397,7 @@
if self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
- self._bot_login_value = (login, now + BOT_LOGIN_TTL)
+ self._bot_login_value = (login, now ^ BOT_LOGIN_TTL)
return login
def approve_pr(........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fd43169cb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:07:13.736192Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'int'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'int'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'int'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'int'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:07:13.736192Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 3.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations + live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7f3f36d279d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='139909426562448'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:313: in _installation_auth
self._prune_rate_limit_metrics()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f3f3397ff70>
def _prune_rate_limit_metrics(self) -> None:
"""Drop rate-limit gauge label sets for installations without live credentials.
Prometheus keeps a child series for every label value ever set. Pruning
whenever new credentials are created bounds the series count by the
credential cache size, so installation churn cannot grow it without
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
> for stale in self._rate_limit_installations + live:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for +: 'set' and 'set'
stampbot/github_client.py:325: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - TypeError: unsupported operand type(s) for +: 'set' and 'set'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.81s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.19s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.21s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.97s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.20s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.29s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Add, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() + start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations * live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7faa7ed4f9d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140370195662224'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:313: in _installation_auth
self._prune_rate_limit_metrics()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7faa7bbcc050>
def _prune_rate_limit_metrics(self) -> None:
"""Drop rate-limit gauge label sets for installations without live credentials.
Prometheus keeps a child series for every label value ever set. Pruning
whenever new credentials are created bounds the series count by the
credential cache size, so installation churn cannot grow it without
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
> for stale in self._rate_limit_installations * live:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for *: 'set' and 'set'
stampbot/github_client.py:325: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - TypeError: unsupported operand type(s) for *: 'set' and 'set'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.81s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.14s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.28s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.19s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.14s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.19s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.25s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.18s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mul, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() * start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations / live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7f24f4fcb9d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='139796651869584'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:313: in _installation_auth
self._prune_rate_limit_metrics()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f24f1d43f70>
def _prune_rate_limit_metrics(self) -> None:
"""Drop rate-limit gauge label sets for installations without live credentials.
Prometheus keeps a child series for every label value ever set. Pruning
whenever new credentials are created bounds the series count by the
credential cache size, so installation churn cannot grow it without
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
> for stale in self._rate_limit_installations / live:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for /: 'set' and 'set'
stampbot/github_client.py:325: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - TypeError: unsupported operand type(s) for /: 'set' and 'set'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.79s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.21s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.34s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.21s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Div, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() / start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations // live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7f0c843379d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='139691681023376'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:313: in _installation_auth
self._prune_rate_limit_metrics()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f0c81147f70>
def _prune_rate_limit_metrics(self) -> None:
"""Drop rate-limit gauge label sets for installations without live credentials.
Prometheus keeps a child series for every label value ever set. Pruning
whenever new credentials are created bounds the series count by the
credential cache size, so installation churn cannot grow it without
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
> for stale in self._rate_limit_installations // live:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for //: 'set' and 'set'
stampbot/github_client.py:325: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - TypeError: unsupported operand type(s) for //: 'set' and 'set'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.85s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.20s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.16s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.14s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_FloorDiv, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() // start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations % live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7fd477b439d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140550464439696'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:313: in _installation_auth
self._prune_rate_limit_metrics()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd4749bbf70>
def _prune_rate_limit_metrics(self) -> None:
"""Drop rate-limit gauge label sets for installations without live credentials.
Prometheus keeps a child series for every label value ever set. Pruning
whenever new credentials are created bounds the series count by the
credential cache size, so installation churn cannot grow it without
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
> for stale in self._rate_limit_installations % live:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for %: 'set' and 'set'
stampbot/github_client.py:325: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - TypeError: unsupported operand type(s) for %: 'set' and 'set'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.85s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.20s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.18s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_Mod, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() % start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations ** live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7fdd4ffbf9d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140588453660048'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:313: in _installation_auth
self._prune_rate_limit_metrics()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fdd4cc07f70>
def _prune_rate_limit_metrics(self) -> None:
"""Drop rate-limit gauge label sets for installations without live credentials.
Prometheus keeps a child series for every label value ever set. Pruning
whenever new credentials are created bounds the series count by the
credential cache size, so installation churn cannot grow it without
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
> for stale in self._rate_limit_installations ** live:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ** or pow(): 'set' and 'set'
stampbot/github_client.py:325: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - TypeError: unsupported operand type(s) for ** or pow(): 'set' and 'set'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
.................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_success _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7f928c8c8190>
def test_approve_pr_success(self):
"""Test successful PR approval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is True
E assert False is True
tests/test_github_client.py:631: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:37:31.400694Z[0m [[31m[1merror [0m] [1mFailed to approve PR #42 in owner/repo: (34, 'Numerical result out of range')[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:468 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}, 'event': "Failed to approve PR #42 in owner/repo: (34, 'Numerical result out of range')", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:37:31.400694Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 121 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
..................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_failure _____________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fa3daf353b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
> pr.create_review(
body=comment,
event="APPROVE",
)
stampbot/github_client.py:437:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().create_review' id='140341723415280'>
args = (), kwargs = {'body': 'Auto-approved by Stampbot', 'event': 'APPROVE'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestApprovePR object at 0x7fa3de07c2d0>
def test_approve_pr_failure(self):
"""Test PR approval failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.create_review.side_effect = Exception("API Error")
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.approve_pr(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:668:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa3daf353b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
pr.create_review(
body=comment,
event="APPROVE",
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "approved"})
set_span_ok(span)
logger.info(
f"Approved PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:462: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_failure - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 122 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
...................................................F
=================================== FAILURES ===================================
________________ TestGetPRHeadSHA.test_get_pr_head_sha_success _________________
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f72a21c8410>
def test_get_pr_head_sha_success(self):
"""Test successful PR head SHA lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.head.sha = "current-head"
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_pr_head_sha(123456, "owner/repo", 42)
> assert result == "current-head"
E AssertionError: assert None == 'current-head'
tests/test_github_client.py:713: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:50:52.867977Z[0m [[31m[1merror [0m] [1mFailed to get head SHA for PR #42 in owner/repo: (34, 'Numerical result out of range')[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:529 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}, 'event': "Failed to get head SHA for PR #42 in owner/repo: (34, 'Numerical result out of range')", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:50:52.867977Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_success - AssertionError: assert None == 'current-head'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 123 passed, 3 deselected, 1 warning in 2.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
....................................................F
=================================== FAILURES ===================================
_________ TestGetPRHeadSHA.test_get_pr_head_sha_returns_none_on_error __________
self = <stampbot.github_client.GitHubAppClient object at 0x7f92fef2def0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:507:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='140269319383968'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f9302080550>
def test_get_pr_head_sha_returns_none_on_error(self):
"""Test PR head SHA lookup returns None on API errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_pr_head_sha(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:745:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f92fef2def0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.head_sha": head_sha})
set_span_ok(span)
return head_sha
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:523: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_returns_none_on_error - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 124 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
.....................................................F
=================================== FAILURES ===================================
______________ TestDismissApproval.test_dismiss_approval_success _______________
self = <tests.test_github_client.TestDismissApproval object at 0x7f1cf3aa0690>
def test_dismiss_approval_success(self):
"""Test successful approval dismissal."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_review = Mock()
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.dismiss_approval(123456, "owner/repo", 42, 999)
> assert result is True
E assert False is True
tests/test_github_client.py:791: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:55:28.329374Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: (34, 'Numerical result out of range')[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}, 'event': "Failed to dismiss approval for PR #42 in owner/repo: (34, 'Numerical result out of range')", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:55:28.329374Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 125 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <stampbot.github_client.GitHubAppClient object at 0x7f41c3b2a7b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
> review.dismiss(message)
stampbot/github_client.py:582:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().get_review().dismiss' id='139920426907168'>
args = ('Duplicate',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 422 {"message": "Validation Failed"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestDismissApproval object at 0x7f41c6d5eea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:864:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f41c3b2a7b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
review.dismiss(message)
except GithubException as e:
# GitHub answers 422 when a review is already dismissed,
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",
review_id,
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"review_id": review_id,
},
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "dismissed"})
set_span_ok(span)
logger.info(
f"Dismissed approval for PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
"review_id": review_id,
},
)
return True
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:623: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
...........................................................F
=================================== FAILURES ===================================
________ TestGetRepoFile.test_get_repo_file_returns_none_for_directory _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7fda24634910>
def test_get_repo_file_returns_none_for_directory(self):
"""Test that get_repo_file returns None when path is a directory."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Return a list (indicating directory)
mock_repo = Mock()
mock_repo.get_contents.return_value = [Mock(), Mock()]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "some/directory")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1021:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fda2153ec10>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'some/directory', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:685: OverflowError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:05:58.150784Z[0m [[33m[1mwarning [0m] [1mGitHub could not read some/directory from owner/repo (OverflowError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}, 'event': 'GitHub could not read some/directory from owner/repo (OverflowError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:05:58.150784Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_for_directory - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 131 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7fb5dd7f4a50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
tests/test_github_client.py:978:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb5da756b70>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = 'main'
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:694: OverflowError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:34:48.152353Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/repo (OverflowError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/repo (OverflowError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:34:48.152353Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <stampbot.github_client.GitHubAppClient object at 0x7f381f432170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139879013561552'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f38226b7100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f381f432170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:706: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 3.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
.F
=================================== FAILURES ===================================
____ TestGetRepoFile.test_get_repo_file_propagates_read_failures[failure2] _____
self = <stampbot.github_client.GitHubAppClient object at 0x7f53d9598050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139998109487792'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E TimeoutError: GitHub read timed out
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: TimeoutError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f53dcc3ee10>
failure = TimeoutError('GitHub read timed out')
@pytest.mark.parametrize(
"failure",
[
GithubException(403, {"message": "Forbidden"}, None),
GithubException(500, {"message": "Error"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_get_repo_file_propagates_read_failures(self, failure):
"""Test non-404 policy failures reach the handler's fail-closed path."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = failure
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(type(failure), match=str(failure)):
> client.get_repo_file(123456, "owner/repo", "stampbot.toml")
tests/test_github_client.py:1410:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f53d9598050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
if repository_is_optionally_missing:
logger.debug(
"Optional policy repository %s was not found",
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
else:
logger.debug(
"Policy file %s was not found in %s",
file_path,
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
return None
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})
set_span_error(span, e)
logger.warning(
"GitHub could not read %s from %s (status %s)",
file_path,
repo_full_name,
e.status,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
raise
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:761: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_read_failures[failure2] - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 145 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fdb0fbc4b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:29:11.793354Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: (34, 'Numerical result out of range')[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: (34, 'Numerical result out of range')", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:29:11.793354Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...F
=================================== FAILURES ===================================
_ TestFindBotApprovalReviews.test_find_bot_approval_reviews_returns_none_on_error _
self = <stampbot.github_client.GitHubAppClient object at 0x7f89c12a7cf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:809:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='140229623493168'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f89c44c4cd0>
def test_find_bot_approval_reviews_returns_none_on_error(self):
"""Test that a listing failure is reported as None, distinct from no reviews."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1523:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f89c12a7cf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
bot_user = self._bot_login()
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
if review.user.login == bot_user and review.state in (
"APPROVED",
"DISMISSED",
):
bot_reviews.append(
{
"id": review.id,
"state": review.state,
"commit_id": getattr(review, "commit_id", None),
}
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)
github_api_requests_total.labels(operation="find_reviews", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span, {"github.reviews_found": len(bot_reviews), "github.bot_user": bot_user}
)
set_span_ok(span)
return bot_reviews
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:843: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_returns_none_on_error - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 147 passed, 3 deselected, 1 warning in 3.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_success ________
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7fad4cca1590>
def test_create_pr_review_comment_success(self):
"""Test successful review comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2098: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:37:13.621376Z[0m [[33m[1mwarning [0m] [1mFailed to post config error review for PR #42 in owner/repo: (34, 'Numerical result out of range')[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:928 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}, 'event': "Failed to post config error review for PR #42 in owner/repo: (34, 'Numerical result out of range')", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:37:13.621376Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 161 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_failure ________
self = <stampbot.github_client.GitHubAppClient object at 0x7fb4dd1acaf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> pr = repo.get_pull(pr_number)
^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:896:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull' id='140414779289488'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7fb4e08256d0>
def test_create_pr_review_comment_failure(self):
"""Test review comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_pull.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
tests/test_github_client.py:2133:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb4dd1acaf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
pr.create_review(body=message, event="COMMENT")
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted config error review on PR #%d in %s",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:922: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_failure - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 162 passed, 3 deselected, 1 warning in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_success ___________
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f09de029810>
def test_create_issue_comment_success(self):
"""Test successful issue comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_issue = Mock()
mock_repo = Mock()
mock_repo.get_issue.return_value = mock_issue
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2187: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:18:29.981843Z[0m [[33m[1mwarning [0m] [1mFailed to post issue comment on #42 in owner/repo: (34, 'Numerical result out of range')[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1009 {'extra': {'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}, 'event': "Failed to post issue comment on #42 in owner/repo: (34, 'Numerical result out of range')", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:18:29.981843Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 163 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_failure ___________
self = <stampbot.github_client.GitHubAppClient object at 0x7f70715d4eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> issue = repo.get_issue(issue_number)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:973:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_issue' id='140120913733888'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f7074b6d950>
def test_create_issue_comment_failure(self):
"""Test issue comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_issue.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
tests/test_github_client.py:2223:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f70715d4eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
issue = repo.get_issue(issue_number)
issue.create_comment(message)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)
github_api_requests_total.labels(operation="issue_comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted issue comment on #%d in %s",
issue_number,
repo_full_name,
extra={
"repo": repo_full_name,
"issue_number": issue_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:1001: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_failure - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 164 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.............F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_true ___________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fbcbe651310>
def test_repo_has_label_true(self):
"""Test repo_has_label returns True when label exists."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.return_value = Mock()
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "autoapprove")
> assert result is True
E assert None is True
tests/test_github_client.py:1937: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_true - assert None is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 157 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fb26fea0a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140404358697648'>
args = ('missing',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fb277481450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "missing")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1975:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb26fea0a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:1066: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
................F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_error __________________
self = <stampbot.github_client.GitHubAppClient object at 0x7f18f638a5d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='139745184464048'>
args = ('autoapprove',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f18f969f950>
def test_repo_has_label_error(self):
"""Test repo_has_label returns None on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "autoapprove")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:2049:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f18f638a5d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
if e.status == 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False
set_span_error(span, e)
logger.debug(
"Could not verify label %s in %s: %s",
label_name,
repo_full_name,
_sanitize_error(e),
extra={
"repo": repo_full_name,
"label": label_name,
"installation_id": installation_id,
"error": _sanitize_error(e),
},
)
return None
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:1091: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_error - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 160 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7fc8b68dce10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:17:46.918433Z[0m [[33m[1mwarning [0m] [1mFailed to check collaborator permission for alice in owner/repo: (34, 'Numerical result out of range')[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1182 {'extra': {'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}, 'event': "Failed to check collaborator permission for alice in owner/repo: (34, 'Numerical result out of range')", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:17:46.918433Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
......F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_error _____________
self = <stampbot.github_client.GitHubAppClient object at 0x7fafe6494410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> permission = repo.get_collaborator_permission(username)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1143:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_collaborator_permission' id='140393459888352'>
args = ('carol',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestUserHasPermission object at 0x7fafe9bbb360>
def test_user_has_permission_error(self):
"""Test permission check returns False on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.user_has_permission(123456, "owner/repo", "carol", "maintain")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1637:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fafe6494410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
permission = repo.get_collaborator_permission(username)
permission_order = ["none", "read", "triage", "write", "maintain", "admin"]
try:
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
permission_index = -1
required_index = 99
has_permission = permission_index >= required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)
github_api_requests_total.labels(operation="get_permission", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.permission": permission,
"github.has_permission": has_permission,
},
)
set_span_ok(span)
return has_permission
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:1174: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_error - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 150 passed, 3 deselected, 1 warning in 3.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........F
=================================== FAILURES ===================================
____________ TestGetUserTeamSlugs.test_get_user_team_slugs_success _____________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7fbbde27d090>
def test_get_user_team_slugs_success(self):
"""Test successful team membership check."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Mock user
mock_user = Mock()
mock_user.login = "alice"
# Mock team that has alice as member
mock_team = Mock()
mock_team.has_in_members.return_value = True
# Mock org
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(
123456, "acme", "alice", ["release-team", "deploy-team"]
)
> assert result == ["release-team", "deploy-team"]
E AssertionError: assert [] == ['release-tea...'deploy-team']
E
E Right contains 2 more items, first extra item: 'release-team'
E
E Full diff:
E + []
E - [
E - 'release-team',
E - 'deploy-team',
E - ]
tests/test_github_client.py:1731: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:46:44.637907Z[0m [[33m[1mwarning [0m] [1mFailed to check team memberships for alice in acme: (34, 'Numerical result out of range')[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1293 {'extra': {'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "(34, 'Numerical result out of range')"}, 'event': "Failed to check team memberships for alice in acme: (34, 'Numerical result out of range')", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:46:44.637907Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_success - AssertionError: assert [] == ['release-tea...'deploy-team']
Right contains 2 more items, first extra item: 'release-team'
Full diff:
+ []
- [
- 'release-team',
- 'deploy-team',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 152 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_Pow, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ** start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
............F
=================================== FAILURES ===================================
_______ TestGetUserTeamSlugs.test_get_user_team_slugs_general_exception ________
self = <stampbot.github_client.GitHubAppClient object at 0x7f90882a25d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
> org = client.get_organization(org_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1229:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization' id='140258663757344'>
args = ('acme',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Network error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f908b5720f0>
def test_get_user_team_slugs_general_exception(self):
"""Test team membership check with general exception."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_organization.side_effect = Exception("Network error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_user_team_slugs(123456, "acme", "alice", ["release-team"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1893:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f90882a25d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
org = client.get_organization(org_name)
user = client.get_user(username)
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)
# Check if user is a member
if team.has_in_members(user): # type: ignore[arg-type]
member_teams.append(team_slug)
logger.debug(
"User %s is a member of team %s",
username,
team_slug,
extra={
"org": org_name,
"username": username,
"team": team_slug,
},
)
except GithubException as e:
# Team not found or no access - skip silently
logger.debug(
"Could not check team %s membership: %s",
team_slug,
e.data.get("message", str(e)) if hasattr(e, "data") else str(e),
extra={
"org": org_name,
"team": team_slug,
"username": username,
},
)
continue
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)
github_api_requests_total.labels(operation="get_user_teams", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.member_teams": len(member_teams),
"github.teams_checked": len(allowed_teams),
},
)
set_span_ok(span)
return member_teams
except Exception as e:
> duration = time.time() ** start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: (34, 'Numerical result out of range')
stampbot/github_client.py:1285: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_general_exception - OverflowError: (34, 'Numerical result out of range')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 156 passed, 3 deselected, 1 warning in 3.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations >> live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7fe5f3f8f9d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140625563633040'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:313: in _installation_auth
self._prune_rate_limit_metrics()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fe5f0d73f70>
def _prune_rate_limit_metrics(self) -> None:
"""Drop rate-limit gauge label sets for installations without live credentials.
Prometheus keeps a child series for every label value ever set. Pruning
whenever new credentials are created bounds the series count by the
credential cache size, so installation churn cannot grow it without
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
> for stale in self._rate_limit_installations >> live:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'set' and 'set'
stampbot/github_client.py:325: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - TypeError: unsupported operand type(s) for >>: 'set' and 'set'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
.................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_success _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7f6c003e8190>
def test_approve_pr_success(self):
"""Test successful PR approval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is True
E assert False is True
tests/test_github_client.py:631: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:28:24.625723Z[0m [[31m[1merror [0m] [1mFailed to approve PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:468 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Failed to approve PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:28:24.625723Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 121 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
..................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_failure _____________________
self = <stampbot.github_client.GitHubAppClient object at 0x7f8c289bd3b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
> pr.create_review(
body=comment,
event="APPROVE",
)
stampbot/github_client.py:437:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().create_review' id='140239948006128'>
args = (), kwargs = {'body': 'Auto-approved by Stampbot', 'event': 'APPROVE'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestApprovePR object at 0x7f8c2bb802d0>
def test_approve_pr_failure(self):
"""Test PR approval failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.create_review.side_effect = Exception("API Error")
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.approve_pr(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:668:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f8c289bd3b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
pr.create_review(
body=comment,
event="APPROVE",
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "approved"})
set_span_ok(span)
logger.info(
f"Approved PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:462: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_failure - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 122 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
...................................................F
=================================== FAILURES ===================================
________________ TestGetPRHeadSHA.test_get_pr_head_sha_success _________________
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f8828178410>
def test_get_pr_head_sha_success(self):
"""Test successful PR head SHA lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.head.sha = "current-head"
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_pr_head_sha(123456, "owner/repo", 42)
> assert result == "current-head"
E AssertionError: assert None == 'current-head'
tests/test_github_client.py:713: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:27:17.269065Z[0m [[31m[1merror [0m] [1mFailed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:529 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Failed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:27:17.269065Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_success - AssertionError: assert None == 'current-head'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 123 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
....................................................F
=================================== FAILURES ===================================
_________ TestGetPRHeadSHA.test_get_pr_head_sha_returns_none_on_error __________
self = <stampbot.github_client.GitHubAppClient object at 0x7efe4d291ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:507:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='139630680681376'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7efe50430550>
def test_get_pr_head_sha_returns_none_on_error(self):
"""Test PR head SHA lookup returns None on API errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_pr_head_sha(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:745:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7efe4d291ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.head_sha": head_sha})
set_span_ok(span)
return head_sha
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:523: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_returns_none_on_error - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 124 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
.....................................................F
=================================== FAILURES ===================================
______________ TestDismissApproval.test_dismiss_approval_success _______________
self = <tests.test_github_client.TestDismissApproval object at 0x7f38589d8690>
def test_dismiss_approval_success(self):
"""Test successful approval dismissal."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_review = Mock()
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.dismiss_approval(123456, "owner/repo", 42, 999)
> assert result is True
E assert False is True
tests/test_github_client.py:791: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:50:57.536211Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Failed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:50:57.536211Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 125 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <stampbot.github_client.GitHubAppClient object at 0x7f2de36967b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
> review.dismiss(message)
stampbot/github_client.py:582:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().get_review().dismiss' id='139835060024864'>
args = ('Duplicate',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 422 {"message": "Validation Failed"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestDismissApproval object at 0x7f2de68f6ea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:864:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f2de36967b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
review.dismiss(message)
except GithubException as e:
# GitHub answers 422 when a review is already dismissed,
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",
review_id,
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"review_id": review_id,
},
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "dismissed"})
set_span_ok(span)
logger.info(
f"Dismissed approval for PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
"review_id": review_id,
},
)
return True
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:623: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
...........................................................F
=================================== FAILURES ===================================
________ TestGetRepoFile.test_get_repo_file_returns_none_for_directory _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f365b66c910>
def test_get_repo_file_returns_none_for_directory(self):
"""Test that get_repo_file returns None when path is a directory."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Return a list (indicating directory)
mock_repo = Mock()
mock_repo.get_contents.return_value = [Mock(), Mock()]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "some/directory")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1021:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f3658522cb0>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'some/directory', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:685: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:21:24.809369Z[0m [[33m[1mwarning [0m] [1mGitHub could not read some/directory from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}, 'event': 'GitHub could not read some/directory from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:21:24.809369Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_for_directory - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 131 passed, 3 deselected, 1 warning in 2.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7feaac04ca50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
tests/test_github_client.py:978:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7feaa8f9ab70>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = 'main'
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:694: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:09:51.456371Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:09:51.456371Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <stampbot.github_client.GitHubAppClient object at 0x7fccc94ae170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140517521814736'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7fcccc6ff100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fccc94ae170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:706: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
.F
=================================== FAILURES ===================================
____ TestGetRepoFile.test_get_repo_file_propagates_read_failures[failure2] _____
self = <stampbot.github_client.GitHubAppClient object at 0x7f10a088c050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139709393408688'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E TimeoutError: GitHub read timed out
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: TimeoutError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f10a3f72e10>
failure = TimeoutError('GitHub read timed out')
@pytest.mark.parametrize(
"failure",
[
GithubException(403, {"message": "Forbidden"}, None),
GithubException(500, {"message": "Error"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_get_repo_file_propagates_read_failures(self, failure):
"""Test non-404 policy failures reach the handler's fail-closed path."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = failure
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(type(failure), match=str(failure)):
> client.get_repo_file(123456, "owner/repo", "stampbot.toml")
tests/test_github_client.py:1410:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f10a088c050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
if repository_is_optionally_missing:
logger.debug(
"Optional policy repository %s was not found",
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
else:
logger.debug(
"Policy file %s was not found in %s",
file_path,
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
return None
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})
set_span_error(span, e)
logger.warning(
"GitHub could not read %s from %s (status %s)",
file_path,
repo_full_name,
e.status,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
raise
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:761: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_read_failures[failure2] - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 145 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fcd457c4b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:00:15.301191Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:00:15.301191Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...F
=================================== FAILURES ===================================
_ TestFindBotApprovalReviews.test_find_bot_approval_reviews_returns_none_on_error _
self = <stampbot.github_client.GitHubAppClient object at 0x7f3065c9bcf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:809:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='139845840089648'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f3068e40cd0>
def test_find_bot_approval_reviews_returns_none_on_error(self):
"""Test that a listing failure is reported as None, distinct from no reviews."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1523:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f3065c9bcf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
bot_user = self._bot_login()
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
if review.user.login == bot_user and review.state in (
"APPROVED",
"DISMISSED",
):
bot_reviews.append(
{
"id": review.id,
"state": review.state,
"commit_id": getattr(review, "commit_id", None),
}
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)
github_api_requests_total.labels(operation="find_reviews", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span, {"github.reviews_found": len(bot_reviews), "github.bot_user": bot_user}
)
set_span_ok(span)
return bot_reviews
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:843: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_returns_none_on_error - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 147 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_success ________
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7fb05afbd590>
def test_create_pr_review_comment_success(self):
"""Test successful review comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2098: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:21:06.157407Z[0m [[33m[1mwarning [0m] [1mFailed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:928 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Failed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:21:06.157407Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 161 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_failure ________
self = <stampbot.github_client.GitHubAppClient object at 0x7f8b131ccaf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> pr = repo.get_pull(pr_number)
^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:896:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull' id='140235295125392'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7f8b166a16d0>
def test_create_pr_review_comment_failure(self):
"""Test review comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_pull.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
tests/test_github_client.py:2133:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f8b131ccaf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
pr.create_review(body=message, event="COMMENT")
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted config error review on PR #%d in %s",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:922: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_failure - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 162 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_success ___________
self = <tests.test_github_client.TestCreateIssueComment object at 0x7fb4e62cd810>
def test_create_issue_comment_success(self):
"""Test successful issue comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_issue = Mock()
mock_repo = Mock()
mock_repo.get_issue.return_value = mock_issue
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2187: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:43:09.122576Z[0m [[33m[1mwarning [0m] [1mFailed to post issue comment on #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1009 {'extra': {'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Failed to post issue comment on #42 in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:43:09.122576Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 163 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_failure ___________
self = <stampbot.github_client.GitHubAppClient object at 0x7f7273ea8eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> issue = repo.get_issue(issue_number)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:973:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_issue' id='140129546660096'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f727b5d9950>
def test_create_issue_comment_failure(self):
"""Test issue comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_issue.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
tests/test_github_client.py:2223:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f7273ea8eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
issue = repo.get_issue(issue_number)
issue.create_comment(message)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)
github_api_requests_total.labels(operation="issue_comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted issue comment on #%d in %s",
issue_number,
repo_full_name,
extra={
"repo": repo_full_name,
"issue_number": issue_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:1001: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_failure - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 164 passed, 3 deselected, 1 warning in 3.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.............F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_true ___________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f91af535310>
def test_repo_has_label_true(self):
"""Test repo_has_label returns True when label exists."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.return_value = Mock()
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "autoapprove")
> assert result is True
E assert None is True
tests/test_github_client.py:1937: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_true - assert None is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 157 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fd12bbf4a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140536358081200'>
args = ('missing',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fd133271450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "missing")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1975:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd12bbf4a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:1066: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
................F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_error __________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fe4f2d9a5d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140621300087984'>
args = ('autoapprove',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fe4f60bf950>
def test_repo_has_label_error(self):
"""Test repo_has_label returns None on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "autoapprove")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:2049:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fe4f2d9a5d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
if e.status == 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False
set_span_error(span, e)
logger.debug(
"Could not verify label %s in %s: %s",
label_name,
repo_full_name,
_sanitize_error(e),
extra={
"repo": repo_full_name,
"label": label_name,
"installation_id": installation_id,
"error": _sanitize_error(e),
},
)
return None
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:1091: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_error - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 160 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7fef9becce10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:08:28.524085Z[0m [[33m[1mwarning [0m] [1mFailed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1182 {'extra': {'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Failed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:08:28.524085Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
......F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_error _____________
self = <stampbot.github_client.GitHubAppClient object at 0x7f75d1c28410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> permission = repo.get_collaborator_permission(username)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1143:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_collaborator_permission' id='140144007491808'>
args = ('carol',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestUserHasPermission object at 0x7f75d55ff360>
def test_user_has_permission_error(self):
"""Test permission check returns False on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.user_has_permission(123456, "owner/repo", "carol", "maintain")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1637:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f75d1c28410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
permission = repo.get_collaborator_permission(username)
permission_order = ["none", "read", "triage", "write", "maintain", "admin"]
try:
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
permission_index = -1
required_index = 99
has_permission = permission_index >= required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)
github_api_requests_total.labels(operation="get_permission", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.permission": permission,
"github.has_permission": has_permission,
},
)
set_span_ok(span)
return has_permission
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:1174: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_error - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 150 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........F
=================================== FAILURES ===================================
____________ TestGetUserTeamSlugs.test_get_user_team_slugs_success _____________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7fdf4f5c9090>
def test_get_user_team_slugs_success(self):
"""Test successful team membership check."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Mock user
mock_user = Mock()
mock_user.login = "alice"
# Mock team that has alice as member
mock_team = Mock()
mock_team.has_in_members.return_value = True
# Mock org
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(
123456, "acme", "alice", ["release-team", "deploy-team"]
)
> assert result == ["release-team", "deploy-team"]
E AssertionError: assert [] == ['release-tea...'deploy-team']
E
E Right contains 2 more items, first extra item: 'release-team'
E
E Full diff:
E + []
E - [
E - 'release-team',
E - 'deploy-team',
E - ]
tests/test_github_client.py:1731: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:34:44.300825Z[0m [[33m[1mwarning [0m] [1mFailed to check team memberships for alice in acme: unsupported operand type(s) for >>: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1293 {'extra': {'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for >>: 'float' and 'float'"}, 'event': "Failed to check team memberships for alice in acme: unsupported operand type(s) for >>: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:34:44.300825Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_success - AssertionError: assert [] == ['release-tea...'deploy-team']
Right contains 2 more items, first extra item: 'release-team'
Full diff:
+ []
- [
- 'release-team',
- 'deploy-team',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 152 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_RShift, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() >> start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
............F
=================================== FAILURES ===================================
_______ TestGetUserTeamSlugs.test_get_user_team_slugs_general_exception ________
self = <stampbot.github_client.GitHubAppClient object at 0x7f50c84ae5d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
> org = client.get_organization(org_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1229:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization' id='139984862098976'>
args = ('acme',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Network error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f50cb7160f0>
def test_get_user_team_slugs_general_exception(self):
"""Test team membership check with general exception."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_organization.side_effect = Exception("Network error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_user_team_slugs(123456, "acme", "alice", ["release-team"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1893:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f50c84ae5d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
org = client.get_organization(org_name)
user = client.get_user(username)
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)
# Check if user is a member
if team.has_in_members(user): # type: ignore[arg-type]
member_teams.append(team_slug)
logger.debug(
"User %s is a member of team %s",
username,
team_slug,
extra={
"org": org_name,
"username": username,
"team": team_slug,
},
)
except GithubException as e:
# Team not found or no access - skip silently
logger.debug(
"Could not check team %s membership: %s",
team_slug,
e.data.get("message", str(e)) if hasattr(e, "data") else str(e),
extra={
"org": org_name,
"team": team_slug,
"username": username,
},
)
continue
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)
github_api_requests_total.labels(operation="get_user_teams", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.member_teams": len(member_teams),
"github.teams_checked": len(allowed_teams),
},
)
set_span_ok(span)
return member_teams
except Exception as e:
> duration = time.time() >> start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'float'
stampbot/github_client.py:1285: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_general_exception - TypeError: unsupported operand type(s) for >>: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 156 passed, 3 deselected, 1 warning in 3.25s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations << live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7fc3a5fdf9d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140478226302352'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:313: in _installation_auth
self._prune_rate_limit_metrics()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fc3a2d4bf70>
def _prune_rate_limit_metrics(self) -> None:
"""Drop rate-limit gauge label sets for installations without live credentials.
Prometheus keeps a child series for every label value ever set. Pruning
whenever new credentials are created bounds the series count by the
credential cache size, so installation churn cannot grow it without
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
> for stale in self._rate_limit_installations << live:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'set' and 'set'
stampbot/github_client.py:325: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - TypeError: unsupported operand type(s) for <<: 'set' and 'set'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
.................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_success _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7f07f6364190>
def test_approve_pr_success(self):
"""Test successful PR approval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is True
E assert False is True
tests/test_github_client.py:631: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:46:14.995796Z[0m [[31m[1merror [0m] [1mFailed to approve PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:468 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Failed to approve PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:46:14.995796Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 121 passed, 3 deselected, 1 warning in 2.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
..................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_failure _____________________
self = <stampbot.github_client.GitHubAppClient object at 0x7f3b736d93b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
> pr.create_review(
body=comment,
event="APPROVE",
)
stampbot/github_client.py:437:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().create_review' id='139893316043504'>
args = (), kwargs = {'body': 'Auto-approved by Stampbot', 'event': 'APPROVE'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestApprovePR object at 0x7f3b7ad882d0>
def test_approve_pr_failure(self):
"""Test PR approval failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.create_review.side_effect = Exception("API Error")
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.approve_pr(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:668:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f3b736d93b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
pr.create_review(
body=comment,
event="APPROVE",
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "approved"})
set_span_ok(span)
logger.info(
f"Approved PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:462: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_failure - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 122 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
...................................................F
=================================== FAILURES ===================================
________________ TestGetPRHeadSHA.test_get_pr_head_sha_success _________________
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f647888c410>
def test_get_pr_head_sha_success(self):
"""Test successful PR head SHA lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.head.sha = "current-head"
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_pr_head_sha(123456, "owner/repo", 42)
> assert result == "current-head"
E AssertionError: assert None == 'current-head'
tests/test_github_client.py:713: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:43:11.386960Z[0m [[31m[1merror [0m] [1mFailed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:529 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Failed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:43:11.386960Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_success - AssertionError: assert None == 'current-head'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 123 passed, 3 deselected, 1 warning in 2.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
....................................................F
=================================== FAILURES ===================================
_________ TestGetPRHeadSHA.test_get_pr_head_sha_returns_none_on_error __________
self = <stampbot.github_client.GitHubAppClient object at 0x7f72002a9ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:507:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='140127605451680'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f72034d0550>
def test_get_pr_head_sha_returns_none_on_error(self):
"""Test PR head SHA lookup returns None on API errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_pr_head_sha(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:745:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f72002a9ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.head_sha": head_sha})
set_span_ok(span)
return head_sha
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:523: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_returns_none_on_error - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 124 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
.....................................................F
=================================== FAILURES ===================================
______________ TestDismissApproval.test_dismiss_approval_success _______________
self = <tests.test_github_client.TestDismissApproval object at 0x7f4535330690>
def test_dismiss_approval_success(self):
"""Test successful approval dismissal."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_review = Mock()
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.dismiss_approval(123456, "owner/repo", 42, 999)
> assert result is True
E assert False is True
tests/test_github_client.py:791: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:52:06.915131Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Failed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:52:06.915131Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 125 passed, 3 deselected, 1 warning in 2.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <stampbot.github_client.GitHubAppClient object at 0x7fc2c57267b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
> review.dismiss(message)
stampbot/github_client.py:582:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().get_review().dismiss' id='140474507277856'>
args = ('Duplicate',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 422 {"message": "Validation Failed"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestDismissApproval object at 0x7fc2c891aea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:864:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fc2c57267b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
review.dismiss(message)
except GithubException as e:
# GitHub answers 422 when a review is already dismissed,
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",
review_id,
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"review_id": review_id,
},
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "dismissed"})
set_span_ok(span)
logger.info(
f"Dismissed approval for PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
"review_id": review_id,
},
)
return True
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:623: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
...........................................................F
=================================== FAILURES ===================================
________ TestGetRepoFile.test_get_repo_file_returns_none_for_directory _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7fbd54088910>
def test_get_repo_file_returns_none_for_directory(self):
"""Test that get_repo_file returns None when path is a directory."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Return a list (indicating directory)
mock_repo = Mock()
mock_repo.get_contents.return_value = [Mock(), Mock()]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "some/directory")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1021:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fbd50e7ec10>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'some/directory', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:685: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:03:36.185919Z[0m [[33m[1mwarning [0m] [1mGitHub could not read some/directory from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}, 'event': 'GitHub could not read some/directory from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:03:36.185919Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_for_directory - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 131 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7fdf3fa9ca50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
tests/test_github_client.py:978:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fdf3c986b70>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = 'main'
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:694: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:04:53.332348Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:04:53.332348Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <stampbot.github_client.GitHubAppClient object at 0x7ff203d66170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140677418087632'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7ff20b0eb100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7ff203d66170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:706: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 3.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
.F
=================================== FAILURES ===================================
____ TestGetRepoFile.test_get_repo_file_propagates_read_failures[failure2] _____
self = <stampbot.github_client.GitHubAppClient object at 0x7f22f0ab4050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139788046914224'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E TimeoutError: GitHub read timed out
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: TimeoutError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f22f417ae10>
failure = TimeoutError('GitHub read timed out')
@pytest.mark.parametrize(
"failure",
[
GithubException(403, {"message": "Forbidden"}, None),
GithubException(500, {"message": "Error"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_get_repo_file_propagates_read_failures(self, failure):
"""Test non-404 policy failures reach the handler's fail-closed path."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = failure
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(type(failure), match=str(failure)):
> client.get_repo_file(123456, "owner/repo", "stampbot.toml")
tests/test_github_client.py:1410:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f22f0ab4050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
if repository_is_optionally_missing:
logger.debug(
"Optional policy repository %s was not found",
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
else:
logger.debug(
"Policy file %s was not found in %s",
file_path,
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
return None
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})
set_span_error(span, e)
logger.warning(
"GitHub could not read %s from %s (status %s)",
file_path,
repo_full_name,
e.status,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
raise
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:761: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_read_failures[failure2] - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 145 passed, 3 deselected, 1 warning in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fc0691bcb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:46:15.779184Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:46:15.779184Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...F
=================================== FAILURES ===================================
_ TestFindBotApprovalReviews.test_find_bot_approval_reviews_returns_none_on_error _
self = <stampbot.github_client.GitHubAppClient object at 0x7f7a66803cf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:809:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='140163679138352'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f7a699eccd0>
def test_find_bot_approval_reviews_returns_none_on_error(self):
"""Test that a listing failure is reported as None, distinct from no reviews."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1523:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f7a66803cf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
bot_user = self._bot_login()
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
if review.user.login == bot_user and review.state in (
"APPROVED",
"DISMISSED",
):
bot_reviews.append(
{
"id": review.id,
"state": review.state,
"commit_id": getattr(review, "commit_id", None),
}
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)
github_api_requests_total.labels(operation="find_reviews", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span, {"github.reviews_found": len(bot_reviews), "github.bot_user": bot_user}
)
set_span_ok(span)
return bot_reviews
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:843: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_returns_none_on_error - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 147 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_success ________
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7f2c1a885590>
def test_create_pr_review_comment_success(self):
"""Test successful review comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2098: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:30:53.318966Z[0m [[33m[1mwarning [0m] [1mFailed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:928 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Failed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:30:53.318966Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 161 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_failure ________
self = <stampbot.github_client.GitHubAppClient object at 0x7f846f6dcaf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> pr = repo.get_pull(pr_number)
^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:896:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull' id='140206779936656'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7f8476d816d0>
def test_create_pr_review_comment_failure(self):
"""Test review comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_pull.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
tests/test_github_client.py:2133:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f846f6dcaf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
pr.create_review(body=message, event="COMMENT")
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted config error review on PR #%d in %s",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:922: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_failure - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 162 passed, 3 deselected, 1 warning in 3.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_success ___________
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f7856351810>
def test_create_issue_comment_success(self):
"""Test successful issue comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_issue = Mock()
mock_repo = Mock()
mock_repo.get_issue.return_value = mock_issue
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2187: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:27:05.887444Z[0m [[33m[1mwarning [0m] [1mFailed to post issue comment on #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1009 {'extra': {'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Failed to post issue comment on #42 in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:27:05.887444Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 163 passed, 3 deselected, 1 warning in 3.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_failure ___________
self = <stampbot.github_client.GitHubAppClient object at 0x7f965b694eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> issue = repo.get_issue(issue_number)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:973:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_issue' id='140283754572032'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f965ecdd950>
def test_create_issue_comment_failure(self):
"""Test issue comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_issue.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
tests/test_github_client.py:2223:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f965b694eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
issue = repo.get_issue(issue_number)
issue.create_comment(message)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)
github_api_requests_total.labels(operation="issue_comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted issue comment on #%d in %s",
issue_number,
repo_full_name,
extra={
"repo": repo_full_name,
"issue_number": issue_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:1001: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_failure - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 164 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.............F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_true ___________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f4e78dd9310>
def test_repo_has_label_true(self):
"""Test repo_has_label returns True when label exists."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.return_value = Mock()
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "autoapprove")
> assert result is True
E assert None is True
tests/test_github_client.py:1937: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_true - assert None is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 157 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fd12bacca50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140536357016240'>
args = ('missing',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fd133181450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "missing")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1975:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd12bacca50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:1066: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 3.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
................F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_error __________________
self = <stampbot.github_client.GitHubAppClient object at 0x7f378d38e5d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='139876567202992'>
args = ('autoapprove',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f37906fb950>
def test_repo_has_label_error(self):
"""Test repo_has_label returns None on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "autoapprove")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:2049:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f378d38e5d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
if e.status == 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False
set_span_error(span, e)
logger.debug(
"Could not verify label %s in %s: %s",
label_name,
repo_full_name,
_sanitize_error(e),
extra={
"repo": repo_full_name,
"label": label_name,
"installation_id": installation_id,
"error": _sanitize_error(e),
},
)
return None
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:1091: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_error - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 160 passed, 3 deselected, 1 warning in 3.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7fea97088e10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:46:46.599031Z[0m [[33m[1mwarning [0m] [1mFailed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1182 {'extra': {'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Failed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:46:46.599031Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
......F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_error _____________
self = <stampbot.github_client.GitHubAppClient object at 0x7f96ed6e8410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> permission = repo.get_collaborator_permission(username)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1143:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_collaborator_permission' id='140286204801248'>
args = ('carol',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestUserHasPermission object at 0x7f96f0d93360>
def test_user_has_permission_error(self):
"""Test permission check returns False on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.user_has_permission(123456, "owner/repo", "carol", "maintain")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1637:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f96ed6e8410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
permission = repo.get_collaborator_permission(username)
permission_order = ["none", "read", "triage", "write", "maintain", "admin"]
try:
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
permission_index = -1
required_index = 99
has_permission = permission_index >= required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)
github_api_requests_total.labels(operation="get_permission", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.permission": permission,
"github.has_permission": has_permission,
},
)
set_span_ok(span)
return has_permission
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:1174: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_error - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 150 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........F
=================================== FAILURES ===================================
____________ TestGetUserTeamSlugs.test_get_user_team_slugs_success _____________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f5327535090>
def test_get_user_team_slugs_success(self):
"""Test successful team membership check."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Mock user
mock_user = Mock()
mock_user.login = "alice"
# Mock team that has alice as member
mock_team = Mock()
mock_team.has_in_members.return_value = True
# Mock org
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(
123456, "acme", "alice", ["release-team", "deploy-team"]
)
> assert result == ["release-team", "deploy-team"]
E AssertionError: assert [] == ['release-tea...'deploy-team']
E
E Right contains 2 more items, first extra item: 'release-team'
E
E Full diff:
E + []
E - [
E - 'release-team',
E - 'deploy-team',
E - ]
tests/test_github_client.py:1731: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:32:35.376254Z[0m [[33m[1mwarning [0m] [1mFailed to check team memberships for alice in acme: unsupported operand type(s) for <<: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1293 {'extra': {'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for <<: 'float' and 'float'"}, 'event': "Failed to check team memberships for alice in acme: unsupported operand type(s) for <<: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:32:35.376254Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_success - AssertionError: assert [] == ['release-tea...'deploy-team']
Right contains 2 more items, first extra item: 'release-team'
Full diff:
+ []
- [
- 'release-team',
- 'deploy-team',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 152 passed, 3 deselected, 1 warning in 2.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_LShift, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() << start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
............F
=================================== FAILURES ===================================
_______ TestGetUserTeamSlugs.test_get_user_team_slugs_general_exception ________
self = <stampbot.github_client.GitHubAppClient object at 0x7f863cc865d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
> org = client.get_organization(org_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1229:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization' id='140214518953504'>
args = ('acme',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Network error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f863fef20f0>
def test_get_user_team_slugs_general_exception(self):
"""Test team membership check with general exception."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_organization.side_effect = Exception("Network error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_user_team_slugs(123456, "acme", "alice", ["release-team"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1893:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f863cc865d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
org = client.get_organization(org_name)
user = client.get_user(username)
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)
# Check if user is a member
if team.has_in_members(user): # type: ignore[arg-type]
member_teams.append(team_slug)
logger.debug(
"User %s is a member of team %s",
username,
team_slug,
extra={
"org": org_name,
"username": username,
"team": team_slug,
},
)
except GithubException as e:
# Team not found or no access - skip silently
logger.debug(
"Could not check team %s membership: %s",
team_slug,
e.data.get("message", str(e)) if hasattr(e, "data") else str(e),
extra={
"org": org_name,
"team": team_slug,
"username": username,
},
)
continue
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)
github_api_requests_total.labels(operation="get_user_teams", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.member_teams": len(member_teams),
"github.teams_checked": len(allowed_teams),
},
)
set_span_ok(span)
return member_teams
except Exception as e:
> duration = time.time() << start_time
^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'float'
stampbot/github_client.py:1285: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_general_exception - TypeError: unsupported operand type(s) for <<: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 156 passed, 3 deselected, 1 warning in 3.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations | live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7f6eb83c7c50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
assert client._rate_limit_installations == {"9001", "9002"}
# Only 9002 still has live credentials.
client._installation_auths[9002] = Mock()
with client._installation_auths_lock:
client._prune_rate_limit_metrics()
assert client._rate_limit_installations == {"9002"}
for gauge in (github_api_rate_limit_remaining, github_api_rate_limit_limit):
labelled = {sample.labels["installation_id"] for sample in gauge.collect()[0].samples}
assert "9001" not in labelled
> assert "9002" in labelled
E AssertionError: assert '9002' in set()
tests/test_github_client.py:530: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert '9002' in set()
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
.................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_success _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7efd8b048190>
def test_approve_pr_success(self):
"""Test successful PR approval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is True
E assert False is True
tests/test_github_client.py:631: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:57:08.157971Z[0m [[31m[1merror [0m] [1mFailed to approve PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:468 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Failed to approve PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:57:08.157971Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 121 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
..................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_failure _____________________
self = <stampbot.github_client.GitHubAppClient object at 0x7f0e0629d3b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
> pr.create_review(
body=comment,
event="APPROVE",
)
stampbot/github_client.py:437:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().create_review' id='139698203837168'>
args = (), kwargs = {'body': 'Auto-approved by Stampbot', 'event': 'APPROVE'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestApprovePR object at 0x7f0e094142d0>
def test_approve_pr_failure(self):
"""Test PR approval failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.create_review.side_effect = Exception("API Error")
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.approve_pr(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:668:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f0e0629d3b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
pr.create_review(
body=comment,
event="APPROVE",
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "approved"})
set_span_ok(span)
logger.info(
f"Approved PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:462: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_failure - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 122 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
...................................................F
=================================== FAILURES ===================================
________________ TestGetPRHeadSHA.test_get_pr_head_sha_success _________________
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f565f960410>
def test_get_pr_head_sha_success(self):
"""Test successful PR head SHA lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.head.sha = "current-head"
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_pr_head_sha(123456, "owner/repo", 42)
> assert result == "current-head"
E AssertionError: assert None == 'current-head'
tests/test_github_client.py:713: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:03:27.463940Z[0m [[31m[1merror [0m] [1mFailed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:529 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Failed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:03:27.463940Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_success - AssertionError: assert None == 'current-head'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 123 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
....................................................F
=================================== FAILURES ===================================
_________ TestGetPRHeadSHA.test_get_pr_head_sha_returns_none_on_error __________
self = <stampbot.github_client.GitHubAppClient object at 0x7f46de685ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:507:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='139942355577760'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f46e1808550>
def test_get_pr_head_sha_returns_none_on_error(self):
"""Test PR head SHA lookup returns None on API errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_pr_head_sha(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:745:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f46de685ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.head_sha": head_sha})
set_span_ok(span)
return head_sha
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:523: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_returns_none_on_error - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 124 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
.....................................................F
=================================== FAILURES ===================================
______________ TestDismissApproval.test_dismiss_approval_success _______________
self = <tests.test_github_client.TestDismissApproval object at 0x7f6614c80690>
def test_dismiss_approval_success(self):
"""Test successful approval dismissal."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_review = Mock()
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.dismiss_approval(123456, "owner/repo", 42, 999)
> assert result is True
E assert False is True
tests/test_github_client.py:791: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:56:17.958126Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Failed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:56:17.958126Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 125 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb9ae19a7b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
> review.dismiss(message)
stampbot/github_client.py:582:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().get_review().dismiss' id='140435460797984'>
args = ('Duplicate',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 422 {"message": "Validation Failed"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestDismissApproval object at 0x7fb9b16e6ea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:864:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb9ae19a7b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
review.dismiss(message)
except GithubException as e:
# GitHub answers 422 when a review is already dismissed,
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",
review_id,
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"review_id": review_id,
},
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "dismissed"})
set_span_ok(span)
logger.info(
f"Dismissed approval for PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
"review_id": review_id,
},
)
return True
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:623: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
...........................................................F
=================================== FAILURES ===================================
________ TestGetRepoFile.test_get_repo_file_returns_none_for_directory _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f0e86a40910>
def test_get_repo_file_returns_none_for_directory(self):
"""Test that get_repo_file returns None when path is a directory."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Return a list (indicating directory)
mock_repo = Mock()
mock_repo.get_contents.return_value = [Mock(), Mock()]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "some/directory")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1021:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f0e8395ec10>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'some/directory', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:685: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:46:04.459448Z[0m [[33m[1mwarning [0m] [1mGitHub could not read some/directory from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}, 'event': 'GitHub could not read some/directory from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:46:04.459448Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_for_directory - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 131 passed, 3 deselected, 1 warning in 2.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f627e2c8a50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
tests/test_github_client.py:978:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f627b0b2b70>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = 'main'
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:694: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:55:12.482811Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:55:12.482811Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <stampbot.github_client.GitHubAppClient object at 0x7f9ee58aa170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140320427341008'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f9ee8ae7100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f9ee58aa170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:706: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
.F
=================================== FAILURES ===================================
____ TestGetRepoFile.test_get_repo_file_propagates_read_failures[failure2] _____
self = <stampbot.github_client.GitHubAppClient object at 0x7f019b7fc050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139644883964592'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E TimeoutError: GitHub read timed out
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: TimeoutError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f01a2f8ee10>
failure = TimeoutError('GitHub read timed out')
@pytest.mark.parametrize(
"failure",
[
GithubException(403, {"message": "Forbidden"}, None),
GithubException(500, {"message": "Error"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_get_repo_file_propagates_read_failures(self, failure):
"""Test non-404 policy failures reach the handler's fail-closed path."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = failure
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(type(failure), match=str(failure)):
> client.get_repo_file(123456, "owner/repo", "stampbot.toml")
tests/test_github_client.py:1410:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f019b7fc050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
if repository_is_optionally_missing:
logger.debug(
"Optional policy repository %s was not found",
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
else:
logger.debug(
"Policy file %s was not found in %s",
file_path,
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
return None
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})
set_span_error(span, e)
logger.warning(
"GitHub could not read %s from %s (status %s)",
file_path,
repo_full_name,
e.status,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
raise
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:761: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_read_failures[failure2] - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 145 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f8269988b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:04:22.513349Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:04:22.513349Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...F
=================================== FAILURES ===================================
_ TestFindBotApprovalReviews.test_find_bot_approval_reviews_returns_none_on_error _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd71f0b3cf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:809:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='140561910723120'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fd7221d8cd0>
def test_find_bot_approval_reviews_returns_none_on_error(self):
"""Test that a listing failure is reported as None, distinct from no reviews."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1523:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd71f0b3cf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
bot_user = self._bot_login()
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
if review.user.login == bot_user and review.state in (
"APPROVED",
"DISMISSED",
):
bot_reviews.append(
{
"id": review.id,
"state": review.state,
"commit_id": getattr(review, "commit_id", None),
}
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)
github_api_requests_total.labels(operation="find_reviews", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span, {"github.reviews_found": len(bot_reviews), "github.bot_user": bot_user}
)
set_span_ok(span)
return bot_reviews
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:843: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_returns_none_on_error - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 147 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_success ________
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7fe6eb089590>
def test_create_pr_review_comment_success(self):
"""Test successful review comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2098: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:28:00.524394Z[0m [[33m[1mwarning [0m] [1mFailed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:928 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Failed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:28:00.524394Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 161 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_failure ________
self = <stampbot.github_client.GitHubAppClient object at 0x7f05acce4af0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> pr = repo.get_pull(pr_number)
^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:896:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull' id='139662348971920'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7f05b023d6d0>
def test_create_pr_review_comment_failure(self):
"""Test review comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_pull.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
tests/test_github_client.py:2133:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f05acce4af0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
pr.create_review(body=message, event="COMMENT")
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted config error review on PR #%d in %s",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:922: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_failure - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 162 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_success ___________
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f2c8c041810>
def test_create_issue_comment_success(self):
"""Test successful issue comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_issue = Mock()
mock_repo = Mock()
mock_repo.get_issue.return_value = mock_issue
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2187: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:37:24.403935Z[0m [[33m[1mwarning [0m] [1mFailed to post issue comment on #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1009 {'extra': {'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Failed to post issue comment on #42 in owner/repo: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:37:24.403935Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 163 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_failure ___________
self = <stampbot.github_client.GitHubAppClient object at 0x7fbfc5060eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> issue = repo.get_issue(issue_number)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:973:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_issue' id='140461620046080'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreateIssueComment object at 0x7fbfc85e9950>
def test_create_issue_comment_failure(self):
"""Test issue comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_issue.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
tests/test_github_client.py:2223:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fbfc5060eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
issue = repo.get_issue(issue_number)
issue.create_comment(message)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)
github_api_requests_total.labels(operation="issue_comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted issue comment on #%d in %s",
issue_number,
repo_full_name,
extra={
"repo": repo_full_name,
"issue_number": issue_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:1001: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_failure - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 164 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.............F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_true ___________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fd3c8a81310>
def test_repo_has_label_true(self):
"""Test repo_has_label returns True when label exists."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.return_value = Mock()
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "autoapprove")
> assert result is True
E assert None is True
tests/test_github_client.py:1937: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_true - assert None is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 157 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fb959da8a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140434053386928'>
args = ('missing',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fb95d241450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "missing")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1975:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb959da8a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:1066: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 3.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
................F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_error __________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fe5830b65d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140623717989552'>
args = ('autoapprove',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fe58633b950>
def test_repo_has_label_error(self):
"""Test repo_has_label returns None on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "autoapprove")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:2049:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fe5830b65d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
if e.status == 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False
set_span_error(span, e)
logger.debug(
"Could not verify label %s in %s: %s",
label_name,
repo_full_name,
_sanitize_error(e),
extra={
"repo": repo_full_name,
"label": label_name,
"installation_id": installation_id,
"error": _sanitize_error(e),
},
)
return None
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:1091: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_error - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 160 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7f5703fa0e10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:52:48.130067Z[0m [[33m[1mwarning [0m] [1mFailed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for |: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1182 {'extra': {'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Failed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:52:48.130067Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
......F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_error _____________
self = <stampbot.github_client.GitHubAppClient object at 0x7f34ca3dc410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> permission = repo.get_collaborator_permission(username)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1143:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_collaborator_permission' id='139864707231968'>
args = ('carol',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestUserHasPermission object at 0x7f34cd91f360>
def test_user_has_permission_error(self):
"""Test permission check returns False on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.user_has_permission(123456, "owner/repo", "carol", "maintain")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1637:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f34ca3dc410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
permission = repo.get_collaborator_permission(username)
permission_order = ["none", "read", "triage", "write", "maintain", "admin"]
try:
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
permission_index = -1
required_index = 99
has_permission = permission_index >= required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)
github_api_requests_total.labels(operation="get_permission", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.permission": permission,
"github.has_permission": has_permission,
},
)
set_span_ok(span)
return has_permission
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:1174: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_error - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 150 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........F
=================================== FAILURES ===================================
____________ TestGetUserTeamSlugs.test_get_user_team_slugs_success _____________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f315fe35090>
def test_get_user_team_slugs_success(self):
"""Test successful team membership check."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Mock user
mock_user = Mock()
mock_user.login = "alice"
# Mock team that has alice as member
mock_team = Mock()
mock_team.has_in_members.return_value = True
# Mock org
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(
123456, "acme", "alice", ["release-team", "deploy-team"]
)
> assert result == ["release-team", "deploy-team"]
E AssertionError: assert [] == ['release-tea...'deploy-team']
E
E Right contains 2 more items, first extra item: 'release-team'
E
E Full diff:
E + []
E - [
E - 'release-team',
E - 'deploy-team',
E - ]
tests/test_github_client.py:1731: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:53:36.939911Z[0m [[33m[1mwarning [0m] [1mFailed to check team memberships for alice in acme: unsupported operand type(s) for |: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1293 {'extra': {'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for |: 'float' and 'float'"}, 'event': "Failed to check team memberships for alice in acme: unsupported operand type(s) for |: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:53:36.939911Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_success - AssertionError: assert [] == ['release-tea...'deploy-team']
Right contains 2 more items, first extra item: 'release-team'
Full diff:
+ []
- [
- 'release-team',
- 'deploy-team',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 152 passed, 3 deselected, 1 warning in 2.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitOr, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() | start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
............F
=================================== FAILURES ===================================
_______ TestGetUserTeamSlugs.test_get_user_team_slugs_general_exception ________
self = <stampbot.github_client.GitHubAppClient object at 0x7ff3207325d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
> org = client.get_organization(org_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1229:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization' id='140682194056736'>
args = ('acme',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Network error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7ff3239260f0>
def test_get_user_team_slugs_general_exception(self):
"""Test team membership check with general exception."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_organization.side_effect = Exception("Network error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_user_team_slugs(123456, "acme", "alice", ["release-team"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1893:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7ff3207325d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
org = client.get_organization(org_name)
user = client.get_user(username)
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)
# Check if user is a member
if team.has_in_members(user): # type: ignore[arg-type]
member_teams.append(team_slug)
logger.debug(
"User %s is a member of team %s",
username,
team_slug,
extra={
"org": org_name,
"username": username,
"team": team_slug,
},
)
except GithubException as e:
# Team not found or no access - skip silently
logger.debug(
"Could not check team %s membership: %s",
team_slug,
e.data.get("message", str(e)) if hasattr(e, "data") else str(e),
extra={
"org": org_name,
"team": team_slug,
"username": username,
},
)
continue
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)
github_api_requests_total.labels(operation="get_user_teams", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.member_teams": len(member_teams),
"github.teams_checked": len(allowed_teams),
},
)
set_span_ok(span)
return member_teams
except Exception as e:
> duration = time.time() | start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'float'
stampbot/github_client.py:1285: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_general_exception - TypeError: unsupported operand type(s) for |: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 156 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations & live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7f95dc7cfc50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
assert client._rate_limit_installations == {"9001", "9002"}
# Only 9002 still has live credentials.
client._installation_auths[9002] = Mock()
with client._installation_auths_lock:
client._prune_rate_limit_metrics()
assert client._rate_limit_installations == {"9002"}
for gauge in (github_api_rate_limit_remaining, github_api_rate_limit_limit):
labelled = {sample.labels["installation_id"] for sample in gauge.collect()[0].samples}
> assert "9001" not in labelled
E AssertionError: assert '9001' not in {'9001'}
tests/test_github_client.py:529: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert '9001' not in {'9001'}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
.................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_success _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7f38db658190>
def test_approve_pr_success(self):
"""Test successful PR approval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is True
E assert False is True
tests/test_github_client.py:631: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:26:02.592802Z[0m [[31m[1merror [0m] [1mFailed to approve PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:468 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Failed to approve PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:26:02.592802Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 121 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
..................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_failure _____________________
self = <stampbot.github_client.GitHubAppClient object at 0x7f2f654a93b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
> pr.create_review(
body=comment,
event="APPROVE",
)
stampbot/github_client.py:437:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().create_review' id='139841539326704'>
args = (), kwargs = {'body': 'Auto-approved by Stampbot', 'event': 'APPROVE'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestApprovePR object at 0x7f2f686782d0>
def test_approve_pr_failure(self):
"""Test PR approval failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.create_review.side_effect = Exception("API Error")
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.approve_pr(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:668:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f2f654a93b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
pr.create_review(
body=comment,
event="APPROVE",
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "approved"})
set_span_ok(span)
logger.info(
f"Approved PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:462: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_failure - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 122 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
...................................................F
=================================== FAILURES ===================================
________________ TestGetPRHeadSHA.test_get_pr_head_sha_success _________________
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7fe03949c410>
def test_get_pr_head_sha_success(self):
"""Test successful PR head SHA lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.head.sha = "current-head"
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_pr_head_sha(123456, "owner/repo", 42)
> assert result == "current-head"
E AssertionError: assert None == 'current-head'
tests/test_github_client.py:713: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:23:54.984058Z[0m [[31m[1merror [0m] [1mFailed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:529 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Failed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:23:54.984058Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_success - AssertionError: assert None == 'current-head'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 123 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
....................................................F
=================================== FAILURES ===================================
_________ TestGetPRHeadSHA.test_get_pr_head_sha_returns_none_on_error __________
self = <stampbot.github_client.GitHubAppClient object at 0x7f43ab0f1ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:507:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='139928608992160'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f43ae180550>
def test_get_pr_head_sha_returns_none_on_error(self):
"""Test PR head SHA lookup returns None on API errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_pr_head_sha(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:745:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f43ab0f1ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.head_sha": head_sha})
set_span_ok(span)
return head_sha
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:523: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_returns_none_on_error - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 124 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
.....................................................F
=================================== FAILURES ===================================
______________ TestDismissApproval.test_dismiss_approval_success _______________
self = <tests.test_github_client.TestDismissApproval object at 0x7fc63de3c690>
def test_dismiss_approval_success(self):
"""Test successful approval dismissal."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_review = Mock()
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.dismiss_approval(123456, "owner/repo", 42, 999)
> assert result is True
E assert False is True
tests/test_github_client.py:791: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:20:46.820931Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Failed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:20:46.820931Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 125 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <stampbot.github_client.GitHubAppClient object at 0x7f8a19e967b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
> review.dismiss(message)
stampbot/github_client.py:582:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().get_review().dismiss' id='140231117354528'>
args = ('Duplicate',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 422 {"message": "Validation Failed"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestDismissApproval object at 0x7f8a1d162ea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:864:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f8a19e967b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
review.dismiss(message)
except GithubException as e:
# GitHub answers 422 when a review is already dismissed,
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",
review_id,
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"review_id": review_id,
},
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "dismissed"})
set_span_ok(span)
logger.info(
f"Dismissed approval for PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
"review_id": review_id,
},
)
return True
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:623: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 3.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
...........................................................F
=================================== FAILURES ===================================
________ TestGetRepoFile.test_get_repo_file_returns_none_for_directory _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7fed03ba4910>
def test_get_repo_file_returns_none_for_directory(self):
"""Test that get_repo_file returns None when path is a directory."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Return a list (indicating directory)
mock_repo = Mock()
mock_repo.get_contents.return_value = [Mock(), Mock()]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "some/directory")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1021:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fed0099acb0>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'some/directory', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:685: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:27:49.468248Z[0m [[33m[1mwarning [0m] [1mGitHub could not read some/directory from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}, 'event': 'GitHub could not read some/directory from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:27:49.468248Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_for_directory - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 131 passed, 3 deselected, 1 warning in 2.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f258785ca50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
tests/test_github_client.py:978:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f258420eb70>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = 'main'
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:694: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:24:00.304861Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:24:00.304861Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <stampbot.github_client.GitHubAppClient object at 0x7ff2c637a170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140680679371984'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7ff2c96d3100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7ff2c637a170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:706: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
.F
=================================== FAILURES ===================================
____ TestGetRepoFile.test_get_repo_file_propagates_read_failures[failure2] _____
self = <stampbot.github_client.GitHubAppClient object at 0x7ffaeaff8050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140715665609392'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E TimeoutError: GitHub read timed out
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: TimeoutError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7ffaee6e2e10>
failure = TimeoutError('GitHub read timed out')
@pytest.mark.parametrize(
"failure",
[
GithubException(403, {"message": "Forbidden"}, None),
GithubException(500, {"message": "Error"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_get_repo_file_propagates_read_failures(self, failure):
"""Test non-404 policy failures reach the handler's fail-closed path."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = failure
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(type(failure), match=str(failure)):
> client.get_repo_file(123456, "owner/repo", "stampbot.toml")
tests/test_github_client.py:1410:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7ffaeaff8050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
if repository_is_optionally_missing:
logger.debug(
"Optional policy repository %s was not found",
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
else:
logger.debug(
"Policy file %s was not found in %s",
file_path,
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
return None
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})
set_span_error(span, e)
logger.warning(
"GitHub could not read %s from %s (status %s)",
file_path,
repo_full_name,
e.status,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
raise
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:761: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_read_failures[failure2] - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 145 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7ff20e12cb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:00:19.292805Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:00:19.292805Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...F
=================================== FAILURES ===================================
_ TestFindBotApprovalReviews.test_find_bot_approval_reviews_returns_none_on_error _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa359587cf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:809:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='140339551482416'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fa35c694cd0>
def test_find_bot_approval_reviews_returns_none_on_error(self):
"""Test that a listing failure is reported as None, distinct from no reviews."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1523:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa359587cf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
bot_user = self._bot_login()
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
if review.user.login == bot_user and review.state in (
"APPROVED",
"DISMISSED",
):
bot_reviews.append(
{
"id": review.id,
"state": review.state,
"commit_id": getattr(review, "commit_id", None),
}
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)
github_api_requests_total.labels(operation="find_reviews", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span, {"github.reviews_found": len(bot_reviews), "github.bot_user": bot_user}
)
set_span_ok(span)
return bot_reviews
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:843: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_returns_none_on_error - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 147 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_success ________
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7f95a4e9d590>
def test_create_pr_review_comment_success(self):
"""Test successful review comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2098: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:16:59.105206Z[0m [[33m[1mwarning [0m] [1mFailed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:928 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Failed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:16:59.105206Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 161 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_failure ________
self = <stampbot.github_client.GitHubAppClient object at 0x7fd2beeccaf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> pr = repo.get_pull(pr_number)
^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:896:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull' id='140543127433104'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7fd2c24a56d0>
def test_create_pr_review_comment_failure(self):
"""Test review comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_pull.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
tests/test_github_client.py:2133:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd2beeccaf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
pr.create_review(body=message, event="COMMENT")
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted config error review on PR #%d in %s",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:922: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_failure - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 162 passed, 3 deselected, 1 warning in 3.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_success ___________
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f2ab6c71810>
def test_create_issue_comment_success(self):
"""Test successful issue comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_issue = Mock()
mock_repo = Mock()
mock_repo.get_issue.return_value = mock_issue
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2187: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:23:15.043395Z[0m [[33m[1mwarning [0m] [1mFailed to post issue comment on #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1009 {'extra': {'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Failed to post issue comment on #42 in owner/repo: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:23:15.043395Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 163 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_failure ___________
self = <stampbot.github_client.GitHubAppClient object at 0x7f2e72ccceb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> issue = repo.get_issue(issue_number)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:973:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_issue' id='139837469419776'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f2e76285950>
def test_create_issue_comment_failure(self):
"""Test issue comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_issue.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
tests/test_github_client.py:2223:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f2e72ccceb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
issue = repo.get_issue(issue_number)
issue.create_comment(message)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)
github_api_requests_total.labels(operation="issue_comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted issue comment on #%d in %s",
issue_number,
repo_full_name,
extra={
"repo": repo_full_name,
"issue_number": issue_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:1001: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_failure - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 164 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.............F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_true ___________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fb8823f5310>
def test_repo_has_label_true(self):
"""Test repo_has_label returns True when label exists."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.return_value = Mock()
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "autoapprove")
> assert result is True
E assert None is True
tests/test_github_client.py:1937: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_true - assert None is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 157 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <stampbot.github_client.GitHubAppClient object at 0x7f4b2c11ca50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='139960839213744'>
args = ('missing',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f4b2f7a5450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "missing")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1975:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f4b2c11ca50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:1066: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
................F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_error __________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fe2eb6625d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140612583586992'>
args = ('autoapprove',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fe2ee84f950>
def test_repo_has_label_error(self):
"""Test repo_has_label returns None on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "autoapprove")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:2049:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fe2eb6625d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
if e.status == 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False
set_span_error(span, e)
logger.debug(
"Could not verify label %s in %s: %s",
label_name,
repo_full_name,
_sanitize_error(e),
extra={
"repo": repo_full_name,
"label": label_name,
"installation_id": installation_id,
"error": _sanitize_error(e),
},
)
return None
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:1091: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_error - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 160 passed, 3 deselected, 1 warning in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7fa7af9c4e10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:42:07.191577Z[0m [[33m[1mwarning [0m] [1mFailed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for &: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1182 {'extra': {'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Failed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:42:07.191577Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
......F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_error _____________
self = <stampbot.github_client.GitHubAppClient object at 0x7f7367be4410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> permission = repo.get_collaborator_permission(username)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1143:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_collaborator_permission' id='140133633028320'>
args = ('carol',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestUserHasPermission object at 0x7f736ad0b360>
def test_user_has_permission_error(self):
"""Test permission check returns False on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.user_has_permission(123456, "owner/repo", "carol", "maintain")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1637:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f7367be4410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
permission = repo.get_collaborator_permission(username)
permission_order = ["none", "read", "triage", "write", "maintain", "admin"]
try:
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
permission_index = -1
required_index = 99
has_permission = permission_index >= required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)
github_api_requests_total.labels(operation="get_permission", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.permission": permission,
"github.has_permission": has_permission,
},
)
set_span_ok(span)
return has_permission
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:1174: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_error - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 150 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........F
=================================== FAILURES ===================================
____________ TestGetUserTeamSlugs.test_get_user_team_slugs_success _____________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f0319335090>
def test_get_user_team_slugs_success(self):
"""Test successful team membership check."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Mock user
mock_user = Mock()
mock_user.login = "alice"
# Mock team that has alice as member
mock_team = Mock()
mock_team.has_in_members.return_value = True
# Mock org
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(
123456, "acme", "alice", ["release-team", "deploy-team"]
)
> assert result == ["release-team", "deploy-team"]
E AssertionError: assert [] == ['release-tea...'deploy-team']
E
E Right contains 2 more items, first extra item: 'release-team'
E
E Full diff:
E + []
E - [
E - 'release-team',
E - 'deploy-team',
E - ]
tests/test_github_client.py:1731: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:48:43.599183Z[0m [[33m[1mwarning [0m] [1mFailed to check team memberships for alice in acme: unsupported operand type(s) for &: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1293 {'extra': {'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for &: 'float' and 'float'"}, 'event': "Failed to check team memberships for alice in acme: unsupported operand type(s) for &: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:48:43.599183Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_success - AssertionError: assert [] == ['release-tea...'deploy-team']
Right contains 2 more items, first extra item: 'release-team'
Full diff:
+ []
- [
- 'release-team',
- 'deploy-team',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 152 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitAnd, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() & start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
............F
=================================== FAILURES ===================================
_______ TestGetUserTeamSlugs.test_get_user_team_slugs_general_exception ________
self = <stampbot.github_client.GitHubAppClient object at 0x7f3e1386e5d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
> org = client.get_organization(org_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1229:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization' id='139904588264992'>
args = ('acme',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Network error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f3e16dea0f0>
def test_get_user_team_slugs_general_exception(self):
"""Test team membership check with general exception."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_organization.side_effect = Exception("Network error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_user_team_slugs(123456, "acme", "alice", ["release-team"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1893:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f3e1386e5d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
org = client.get_organization(org_name)
user = client.get_user(username)
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)
# Check if user is a member
if team.has_in_members(user): # type: ignore[arg-type]
member_teams.append(team_slug)
logger.debug(
"User %s is a member of team %s",
username,
team_slug,
extra={
"org": org_name,
"username": username,
"team": team_slug,
},
)
except GithubException as e:
# Team not found or no access - skip silently
logger.debug(
"Could not check team %s membership: %s",
team_slug,
e.data.get("message", str(e)) if hasattr(e, "data") else str(e),
extra={
"org": org_name,
"team": team_slug,
"username": username,
},
)
continue
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)
github_api_requests_total.labels(operation="get_user_teams", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.member_teams": len(member_teams),
"github.teams_checked": len(allowed_teams),
},
)
set_span_ok(span)
return member_teams
except Exception as e:
> duration = time.time() & start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'float'
stampbot/github_client.py:1285: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_general_exception - TypeError: unsupported operand type(s) for &: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 156 passed, 3 deselected, 1 warning in 3.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in self._rate_limit_installations ^ live:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 1, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -439,7 +439,7 @@
event="APPROVE",
)
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
........................................................................ [ 15%]
.................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_success _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7efff11e0190>
def test_approve_pr_success(self):
"""Test successful PR approval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is True
E assert False is True
tests/test_github_client.py:631: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:05:48.498675Z[0m [[31m[1merror [0m] [1mFailed to approve PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:468 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Failed to approve PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:05:48.498675Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 121 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -459,7 +459,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()
........................................................................ [ 15%]
..................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_failure _____________________
self = <stampbot.github_client.GitHubAppClient object at 0x7f23889013b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
> pr.create_review(
body=comment,
event="APPROVE",
)
stampbot/github_client.py:437:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().create_review' id='139790585987824'>
args = (), kwargs = {'body': 'Auto-approved by Stampbot', 'event': 'APPROVE'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestApprovePR object at 0x7f238ba242d0>
def test_approve_pr_failure(self):
"""Test PR approval failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.create_review.side_effect = Exception("API Error")
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.approve_pr(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:668:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f23889013b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
pr.create_review(
body=comment,
event="APPROVE",
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "approved"})
set_span_ok(span)
logger.info(
f"Approved PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:462: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_failure - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 122 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 3, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -508,7 +508,7 @@
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
........................................................................ [ 15%]
...................................................F
=================================== FAILURES ===================================
________________ TestGetPRHeadSHA.test_get_pr_head_sha_success _________________
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f7130470410>
def test_get_pr_head_sha_success(self):
"""Test successful PR head SHA lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.head.sha = "current-head"
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_pr_head_sha(123456, "owner/repo", 42)
> assert result == "current-head"
E AssertionError: assert None == 'current-head'
tests/test_github_client.py:713: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:49:23.325628Z[0m [[31m[1merror [0m] [1mFailed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:529 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Failed to get head SHA for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:49:23.325628Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_success - AssertionError: assert None == 'current-head'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 123 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 4, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -520,7 +520,7 @@
return head_sha
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()
........................................................................ [ 15%]
....................................................F
=================================== FAILURES ===================================
_________ TestGetPRHeadSHA.test_get_pr_head_sha_returns_none_on_error __________
self = <stampbot.github_client.GitHubAppClient object at 0x7f4ab5c95ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:507:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='139958854020000'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f4ab8e80550>
def test_get_pr_head_sha_returns_none_on_error(self):
"""Test PR head SHA lookup returns None on API errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_pr_head_sha(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:745:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f4ab5c95ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.head_sha": head_sha})
set_span_ok(span)
return head_sha
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:523: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_returns_none_on_error - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 124 passed, 3 deselected, 1 warning in 2.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -599,7 +599,7 @@
},
)
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
........................................................................ [ 15%]
.....................................................F
=================================== FAILURES ===================================
______________ TestDismissApproval.test_dismiss_approval_success _______________
self = <tests.test_github_client.TestDismissApproval object at 0x7f05d890c690>
def test_dismiss_approval_success(self):
"""Test successful approval dismissal."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_review = Mock()
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.dismiss_approval(123456, "owner/repo", 42, 999)
> assert result is True
E assert False is True
tests/test_github_client.py:791: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:44:04.984037Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Failed to dismiss approval for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:44:04.984037Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 125 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 6, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -620,7 +620,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()
........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <stampbot.github_client.GitHubAppClient object at 0x7fdf6ddba7b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
> review.dismiss(message)
stampbot/github_client.py:582:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().get_review().dismiss' id='140597592225312'>
args = ('Duplicate',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 422 {"message": "Validation Failed"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestDismissApproval object at 0x7fdf710deea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:864:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fdf6ddba7b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
review.dismiss(message)
except GithubException as e:
# GitHub answers 422 when a review is already dismissed,
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",
review_id,
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"review_id": review_id,
},
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "dismissed"})
set_span_ok(span)
logger.info(
f"Dismissed approval for PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
"review_id": review_id,
},
)
return True
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:623: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 7, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -682,7 +682,7 @@
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)........................................................................ [ 15%]
...........................................................F
=================================== FAILURES ===================================
________ TestGetRepoFile.test_get_repo_file_returns_none_for_directory _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f2236080910>
def test_get_repo_file_returns_none_for_directory(self):
"""Test that get_repo_file returns None when path is a directory."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Return a list (indicating directory)
mock_repo = Mock()
mock_repo.get_contents.return_value = [Mock(), Mock()]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "some/directory")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1021:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f2232e9ec10>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'some/directory', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:685: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:34:42.375886Z[0m [[33m[1mwarning [0m] [1mGitHub could not read some/directory from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'some/directory', 'installation_id': 123456}, 'event': 'GitHub could not read some/directory from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:34:42.375886Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_for_directory - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 131 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 8, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -691,7 +691,7 @@
set_span_ok(span)
return None
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
........................................................................ [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7ff2599c4a50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
tests/test_github_client.py:978:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7ff256846b70>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = 'main'
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:694: TypeError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:42:33.240977Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/repo (TypeError)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:766 {'extra': {'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/repo (TypeError)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:42:33.240977Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 9, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -703,7 +703,7 @@
return content.decoded_content.decode("utf-8")
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <stampbot.github_client.GitHubAppClient object at 0x7f4564c56170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139936014431440'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f4567e8f100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f4564c56170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:706: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 3.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -758,7 +758,7 @@
raise
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})........................................................................ [ 15%]
........................................................................ [ 31%]
.F
=================================== FAILURES ===================================
____ TestGetRepoFile.test_get_repo_file_propagates_read_failures[failure2] _____
self = <stampbot.github_client.GitHubAppClient object at 0x7fa376288050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140340041749168'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E TimeoutError: GitHub read timed out
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: TimeoutError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7fa379886e10>
failure = TimeoutError('GitHub read timed out')
@pytest.mark.parametrize(
"failure",
[
GithubException(403, {"message": "Forbidden"}, None),
GithubException(500, {"message": "Error"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_get_repo_file_propagates_read_failures(self, failure):
"""Test non-404 policy failures reach the handler's fail-closed path."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = failure
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(type(failure), match=str(failure)):
> client.get_repo_file(123456, "owner/repo", "stampbot.toml")
tests/test_github_client.py:1410:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa376288050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
if repository_is_optionally_missing:
logger.debug(
"Optional policy repository %s was not found",
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
else:
logger.debug(
"Policy file %s was not found in %s",
file_path,
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
return None
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})
set_span_error(span, e)
logger.warning(
"GitHub could not read %s from %s (status %s)",
file_path,
repo_full_name,
e.status,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
raise
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:761: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_read_failures[failure2] - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 145 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 11, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -824,7 +824,7 @@
}
)
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f45447f4b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:35:28.388954Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:35:28.388954Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -840,7 +840,7 @@
return bot_reviews
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...F
=================================== FAILURES ===================================
_ TestFindBotApprovalReviews.test_find_bot_approval_reviews_returns_none_on_error _
self = <stampbot.github_client.GitHubAppClient object at 0x7fc78ffabcf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:809:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='140495086481968'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fc797240cd0>
def test_find_bot_approval_reviews_returns_none_on_error(self):
"""Test that a listing failure is reported as None, distinct from no reviews."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1523:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fc78ffabcf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
bot_user = self._bot_login()
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
if review.user.login == bot_user and review.state in (
"APPROVED",
"DISMISSED",
):
bot_reviews.append(
{
"id": review.id,
"state": review.state,
"commit_id": getattr(review, "commit_id", None),
}
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)
github_api_requests_total.labels(operation="find_reviews", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span, {"github.reviews_found": len(bot_reviews), "github.bot_user": bot_user}
)
set_span_ok(span)
return bot_reviews
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:843: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_returns_none_on_error - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 147 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -897,7 +897,7 @@
pr.create_review(body=message, event="COMMENT")
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_success ________
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7f3c03a69590>
def test_create_pr_review_comment_success(self):
"""Test successful review comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2098: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:47:15.114106Z[0m [[33m[1mwarning [0m] [1mFailed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:928 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Failed to post config error review for PR #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:47:15.114106Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 161 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 14, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -919,7 +919,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_failure ________
self = <stampbot.github_client.GitHubAppClient object at 0x7f8a3fc60af0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> pr = repo.get_pull(pr_number)
^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:896:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull' id='140231825092496'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7f8a473816d0>
def test_create_pr_review_comment_failure(self):
"""Test review comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_pull.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
tests/test_github_client.py:2133:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f8a3fc60af0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
pr.create_review(body=message, event="COMMENT")
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted config error review on PR #%d in %s",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:922: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_failure - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 162 passed, 3 deselected, 1 warning in 3.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 15, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -974,7 +974,7 @@
issue.create_comment(message)
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
...................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_success ___________
self = <tests.test_github_client.TestCreateIssueComment object at 0x7fabad3ed810>
def test_create_issue_comment_success(self):
"""Test successful issue comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_issue = Mock()
mock_repo = Mock()
mock_repo.get_issue.return_value = mock_issue
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2187: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:44:48.066288Z[0m [[33m[1mwarning [0m] [1mFailed to post issue comment on #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1009 {'extra': {'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Failed to post issue comment on #42 in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:44:48.066288Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 163 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 16, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -998,7 +998,7 @@
return True
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_failure ___________
self = <stampbot.github_client.GitHubAppClient object at 0x7fbfbd6c0eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> issue = repo.get_issue(issue_number)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:973:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_issue' id='140461492234496'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreateIssueComment object at 0x7fbfc0ccd950>
def test_create_issue_comment_failure(self):
"""Test issue comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_issue.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
tests/test_github_client.py:2223:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fbfbd6c0eb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
issue = repo.get_issue(issue_number)
issue.create_comment(message)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)
github_api_requests_total.labels(operation="issue_comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted issue comment on #%d in %s",
issue_number,
repo_full_name,
extra={
"repo": repo_full_name,
"issue_number": issue_number,
"installation_id": installation_id,
},
)
return True
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:1001: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_failure - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 164 passed, 3 deselected, 1 warning in 3.41s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 17, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1052,7 +1052,7 @@
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
.............F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_true ___________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fb15a849310>
def test_repo_has_label_true(self):
"""Test repo_has_label returns True when label exists."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.return_value = Mock()
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "autoapprove")
> assert result is True
E assert None is True
tests/test_github_client.py:1937: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_true - assert None is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 157 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 18, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1063,7 +1063,7 @@
return True
except GithubException as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <stampbot.github_client.GitHubAppClient object at 0x7f96576a4a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140283688637104'>
args = ('missing',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f965ab6d450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "missing")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1975:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f96576a4a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:1066: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 19, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1088,7 +1088,7 @@
return None
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
................F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_error __________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fe612a6a5d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140626130402480'>
args = ('autoapprove',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fe615cd7950>
def test_repo_has_label_error(self):
"""Test repo_has_label returns None on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "autoapprove")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:2049:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fe612a6a5d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
if e.status == 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False
set_span_error(span, e)
logger.debug(
"Could not verify label %s in %s: %s",
label_name,
repo_full_name,
_sanitize_error(e),
extra={
"repo": repo_full_name,
"label": label_name,
"installation_id": installation_id,
"error": _sanitize_error(e),
},
)
return None
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:1091: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_error - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 160 passed, 3 deselected, 1 warning in 3.16s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 20, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1151,7 +1151,7 @@
has_permission = permission_index >= required_index
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7f4127a04e10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:29:30.871709Z[0m [[33m[1mwarning [0m] [1mFailed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1182 {'extra': {'repo': 'owner/repo', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Failed to check collaborator permission for alice in owner/repo: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:29:30.871709Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 21, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1171,7 +1171,7 @@
return has_permission
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
......F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_error _____________
self = <stampbot.github_client.GitHubAppClient object at 0x7fdfa29c0410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> permission = repo.get_collaborator_permission(username)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1143:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_collaborator_permission' id='140598482710752'>
args = ('carol',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestUserHasPermission object at 0x7fdfa60b7360>
def test_user_has_permission_error(self):
"""Test permission check returns False on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.user_has_permission(123456, "owner/repo", "carol", "maintain")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1637:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fdfa29c0410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
permission = repo.get_collaborator_permission(username)
permission_order = ["none", "read", "triage", "write", "maintain", "admin"]
try:
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
permission_index = -1
required_index = 99
has_permission = permission_index >= required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)
github_api_requests_total.labels(operation="get_permission", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.permission": permission,
"github.has_permission": has_permission,
},
)
set_span_ok(span)
return has_permission
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:1174: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_error - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 150 passed, 3 deselected, 1 warning in 3.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 22, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1262,7 +1262,7 @@
)
continue
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
........F
=================================== FAILURES ===================================
____________ TestGetUserTeamSlugs.test_get_user_team_slugs_success _____________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f2eebb61090>
def test_get_user_team_slugs_success(self):
"""Test successful team membership check."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Mock user
mock_user = Mock()
mock_user.login = "alice"
# Mock team that has alice as member
mock_team = Mock()
mock_team.has_in_members.return_value = True
# Mock org
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(
123456, "acme", "alice", ["release-team", "deploy-team"]
)
> assert result == ["release-team", "deploy-team"]
E AssertionError: assert [] == ['release-tea...'deploy-team']
E
E Right contains 2 more items, first extra item: 'release-team'
E
E Full diff:
E + []
E - [
E - 'release-team',
E - 'deploy-team',
E - ]
tests/test_github_client.py:1731: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:14:26.449587Z[0m [[33m[1mwarning [0m] [1mFailed to check team memberships for alice in acme: unsupported operand type(s) for ^: 'float' and 'float'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1293 {'extra': {'org': 'acme', 'username': 'alice', 'installation_id': 123456, 'error': "unsupported operand type(s) for ^: 'float' and 'float'"}, 'event': "Failed to check team memberships for alice in acme: unsupported operand type(s) for ^: 'float' and 'float'", 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:14:26.449587Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_success - AssertionError: assert [] == ['release-tea...'deploy-team']
Right contains 2 more items, first extra item: 'release-team'
Full diff:
+ []
- [
- 'release-team',
- 'deploy-team',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 152 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Sub_BitXor, occurrence: 23, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1282,7 +1282,7 @@
return member_teams
except Exception as e:
- duration = time.time() - start_time
+ duration = time.time() ^ start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)........................................................................ [ 15%]
........................................................................ [ 31%]
............F
=================================== FAILURES ===================================
_______ TestGetUserTeamSlugs.test_get_user_team_slugs_general_exception ________
self = <stampbot.github_client.GitHubAppClient object at 0x7f3c6e27e5d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
> org = client.get_organization(org_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1229:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization' id='139897518356000'>
args = ('acme',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Network error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f3c7159a0f0>
def test_get_user_team_slugs_general_exception(self):
"""Test team membership check with general exception."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_organization.side_effect = Exception("Network error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_user_team_slugs(123456, "acme", "alice", ["release-team"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1893:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f3c6e27e5d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
org = client.get_organization(org_name)
user = client.get_user(username)
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)
# Check if user is a member
if team.has_in_members(user): # type: ignore[arg-type]
member_teams.append(team_slug)
logger.debug(
"User %s is a member of team %s",
username,
team_slug,
extra={
"org": org_name,
"username": username,
"team": team_slug,
},
)
except GithubException as e:
# Team not found or no access - skip silently
logger.debug(
"Could not check team %s membership: %s",
team_slug,
e.data.get("message", str(e)) if hasattr(e, "data") else str(e),
extra={
"org": org_name,
"team": team_slug,
"username": username,
},
)
continue
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)
github_api_requests_total.labels(operation="get_user_teams", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.member_teams": len(member_teams),
"github.teams_checked": len(allowed_teams),
},
)
set_span_ok(span)
return member_teams
except Exception as e:
> duration = time.time() ^ start_time
^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'float'
stampbot/github_client.py:1285: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_general_exception - TypeError: unsupported operand type(s) for ^: 'float' and 'float'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 156 passed, 3 deselected, 1 warning in 3.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Mul_Add, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 + 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Add, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, +, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, +, optional_repo: bool) -> NotFoundKind:
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Add, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ +,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E +,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Sub, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 - 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f7b0e493770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f7b09a968b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds -960 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds -960 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Sub, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, -, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, -, optional_repo: bool) -> NotFoundKind:
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Sub, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ -,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E -,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Div, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 / 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fdb83e6b770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fdb7f4aa8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 0.0625 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 0.0625 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.87s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Mul_Div, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, /, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Div, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ /,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository......................................................................... [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f5e10088a50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
E TypeError: GitHubAppClient.get_repo_file() got some positional-only arguments passed as keyword arguments: 'ref'
tests/test_github_client.py:978: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - TypeError: GitHubAppClient.get_repo_file() got some positional-only arguments passed as keyword arguments: 'ref'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_FloorDiv, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 // 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f49d8fcb770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f49d45ca8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 0 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 0 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_FloorDiv, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, //, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, //, optional_repo: bool) -> NotFoundKind:
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_FloorDiv, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ //,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E //,
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Mod, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 % 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fb719bb7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb71518e8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 64 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 64 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Mod, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, %, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, %, optional_repo: bool) -> NotFoundKind:
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Mod, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ %,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E %,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Pow, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 ** 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f7662fa6780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-627/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f765bdbe300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
> pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: cannot fit 'int' into an index-sized integer
stampbot/github_client.py:241: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - OverflowError: cannot fit 'int' into an index-sized integer
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Pow, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, **, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, **, optional_repo: bool) -> NotFoundKind:
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Pow, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ **,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E **,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_RShift, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 >> 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f8eac8a3770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f8ea3d9a8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 0 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 0 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_RShift, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, >>, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, >>, optional_repo: bool) -> NotFoundKind:
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_RShift, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ >>,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E >>,
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_LShift, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 << 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f8182c62780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-30/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f817fb2e300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
> pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E OverflowError: cannot fit 'int' into an index-sized integer
stampbot/github_client.py:241: OverflowError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - OverflowError: cannot fit 'int' into an index-sized integer
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_LShift, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, <<, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, <<, optional_repo: bool) -> NotFoundKind:
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_LShift, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ <<,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E <<,
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.62s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Mul_BitOr, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 | 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitOr, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, |, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, |, optional_repo: bool) -> NotFoundKind:
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitOr, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ |,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E |,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitAnd, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 & 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f66fcbef770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f66f829a8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 0 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 0 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitAnd, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, &, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, &, optional_repo: bool) -> NotFoundKind:
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitAnd, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ &,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E &,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.66s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceBinaryOperator_Mul_BitXor, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 ^ 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitXor, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -111,7 +111,7 @@
NotFoundKind = Literal["repository", "file", "unconfirmed"]
-def _classify_not_found(repo: Repository, ref: str | None, *, optional_repo: bool) -> NotFoundKind:
+def _classify_not_found(repo: Repository, ref: str | None, ^, optional_repo: bool) -> NotFoundKind:
"""Decide what a contents 404 means.
An optional fallback repository may itself be missing, which one repository
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 114
E def _classify_not_found(repo: Repository, ref: str | None, ^, optional_repo: bool) -> NotFoundKind:
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitXor, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -646,7 +646,7 @@
repo_full_name: str,
file_path: str,
ref: str | None = None,
- *,
+ ^,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
E File "/home/runner/work/stampbot/stampbot/stampbot/github_client.py", line 649
E ^,
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.73s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 0, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -102,7 +102,7 @@
try:
repo.complete()
except GithubException as e:
- return e.status == 404
+ return e.status != 404
except Exception:
return False
return False........................................................................ [ 15%]
...................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_accepts_missing_optional_repository_with_lazy_client _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f2275039400>
def test_get_repo_file_accepts_missing_optional_repository_with_lazy_client(self):
"""Test a lazy client confirms the optional repository itself is missing."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_integration_cls.return_value = mock_integration
# Lazy get_repo never raises. The file read and the root read both
# 404, and only the repository request proves the repo is absent.
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
GithubException(404, {"message": "Not Found"}, None),
]
mock_repo.complete.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/.github",
"stampbot.toml",
missing_repository_is_optional=True,
)
tests/test_github_client.py:1236:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139785911713872'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:24:07.814542Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/.github (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/.github (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:24:07.814542Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_accepts_missing_optional_repository_with_lazy_client - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 139 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 1, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if e.status != 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7fafde1fb100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140393270801616'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:42:37.555255Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:42:37.555255Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 3.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -710,7 +710,7 @@
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
- repository_is_optionally_missing = not_found == "repository"
+ repository_is_optionally_missing = not_found != "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()........................................................................ [ 15%]
.............................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_propagates_unconfirmed_not_found[root_result0] _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f09a0e7f230>
root_result = GithubException(404 {"message": "Not Found"})
@pytest.mark.parametrize(
"root_result",
[
GithubException(404, {"message": "Not Found"}, None),
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub root read timed out"),
Mock(),
],
)
def test_get_repo_file_propagates_unconfirmed_not_found(self, root_result):
"""Test a contents 404 fails closed unless the same ref remains readable."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo.get_contents.side_effect = [first, root_result]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(GithubException) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE GithubException
tests/test_github_client.py:1110: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_unconfirmed_not_found[root_result0] - Failed: DID NOT RAISE GithubException
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 133 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 3, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -711,7 +711,7 @@
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
- file_is_confirmed_missing = not_found == "file"
+ file_is_confirmed_missing = not_found != "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7ff9c443f100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140710658307280'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:46:09.706048Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:46:09.706048Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 3.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 4, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -812,7 +812,7 @@
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
- if review.user.login == bot_user and review.state in (
+ if review.user.login != bot_user and review.state in (
"APPROVED",
"DISMISSED",
):........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fde36b60b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert [{'id': 456, ...2321820528'>}] == [{'id': 123, ...d': 'oldsha'}]
E
E At index 0 diff: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='140592321820528'>} != {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
E Right contains one more item: {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}
E
E Full diff:
E [
E {
E - 'id': 123,
E ? ^^^
E + 'id': 456,
E ? ^^^
E 'state': 'APPROVED',
E + 'commit_id': <Mock name='mock.commit_id' id='140592321820528'>,
E - 'commit_id': 'headsha',
E - },
E - {
E - 'id': 124,
E - 'state': 'DISMISSED',
E - 'commit_id': 'oldsha',
E },
E ]
tests/test_github_client.py:1481: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert [{'id': 456, ...2321820528'>}] == [{'id': 123, ...d': 'oldsha'}]
At index 0 diff: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='140592321820528'>} != {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
Right contains one more item: {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}
Full diff:
[
{
- 'id': 123,
? ^^^
+ 'id': 456,
? ^^^
'state': 'APPROVED',
+ 'commit_id': <Mock name='mock.commit_id' id='140592321820528'>,
- 'commit_id': 'headsha',
- },
- {
- 'id': 124,
- 'state': 'DISMISSED',
- 'commit_id': 'oldsha',
},
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 5, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1067,7 +1067,7 @@
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
- if e.status == 404:
+ if e.status != 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f67223f1450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "missing")
> assert result is False
E assert None is False
tests/test_github_client.py:1977: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - assert None is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 0, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -102,7 +102,7 @@
try:
repo.complete()
except GithubException as e:
- return e.status == 404
+ return e.status < 404
except Exception:
return False
return False........................................................................ [ 15%]
...................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_accepts_missing_optional_repository_with_lazy_client _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f77f73fd400>
def test_get_repo_file_accepts_missing_optional_repository_with_lazy_client(self):
"""Test a lazy client confirms the optional repository itself is missing."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_integration_cls.return_value = mock_integration
# Lazy get_repo never raises. The file read and the root read both
# 404, and only the repository request proves the repo is absent.
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
GithubException(404, {"message": "Not Found"}, None),
]
mock_repo.complete.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/.github",
"stampbot.toml",
missing_repository_is_optional=True,
)
tests/test_github_client.py:1236:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140153099796560'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:31:17.651361Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/.github (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/.github (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:31:17.651361Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_accepts_missing_optional_repository_with_lazy_client - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 139 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 1, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if e.status < 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f414dcf3100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139918396044496'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:41:06.556022Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:41:06.556022Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -710,7 +710,7 @@
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
- repository_is_optionally_missing = not_found == "repository"
+ repository_is_optionally_missing = not_found < "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()........................................................................ [ 15%]
...................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_accepts_missing_optional_repository_with_lazy_client _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f991c049400>
def test_get_repo_file_accepts_missing_optional_repository_with_lazy_client(self):
"""Test a lazy client confirms the optional repository itself is missing."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_integration_cls.return_value = mock_integration
# Lazy get_repo never raises. The file read and the root read both
# 404, and only the repository request proves the repo is absent.
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
GithubException(404, {"message": "Not Found"}, None),
]
mock_repo.complete.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/.github",
"stampbot.toml",
missing_repository_is_optional=True,
)
tests/test_github_client.py:1236:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140295516782672'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:26:54.122315Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/.github (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/.github (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:26:54.122315Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_accepts_missing_optional_repository_with_lazy_client - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 139 passed, 3 deselected, 1 warning in 3.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 3, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -711,7 +711,7 @@
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
- file_is_confirmed_missing = not_found == "file"
+ file_is_confirmed_missing = not_found < "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f81345a3100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140192847099088'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:54:18.030556Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:54:18.030556Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 4, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -812,7 +812,7 @@
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
- if review.user.login == bot_user and review.state in (
+ if review.user.login < bot_user and review.state in (
"APPROVED",
"DISMISSED",
):........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f3c876dcb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert [{'id': 456, ...7821788016'>}] == [{'id': 123, ...d': 'oldsha'}]
E
E At index 0 diff: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='139897821788016'>} != {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
E Right contains one more item: {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}
E
E Full diff:
E [
E {
E - 'id': 123,
E ? ^^^
E + 'id': 456,
E ? ^^^
E 'state': 'APPROVED',
E + 'commit_id': <Mock name='mock.commit_id' id='139897821788016'>,
E - 'commit_id': 'headsha',
E - },
E - {
E - 'id': 124,
E - 'state': 'DISMISSED',
E - 'commit_id': 'oldsha',
E },
E ]
tests/test_github_client.py:1481: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert [{'id': 456, ...7821788016'>}] == [{'id': 123, ...d': 'oldsha'}]
At index 0 diff: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='139897821788016'>} != {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
Right contains one more item: {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}
Full diff:
[
{
- 'id': 123,
? ^^^
+ 'id': 456,
? ^^^
'state': 'APPROVED',
+ 'commit_id': <Mock name='mock.commit_id' id='139897821788016'>,
- 'commit_id': 'headsha',
- },
- {
- 'id': 124,
- 'state': 'DISMISSED',
- 'commit_id': 'oldsha',
},
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 5, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1067,7 +1067,7 @@
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
- if e.status == 404:
+ if e.status < 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fac59e7d450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "missing")
> assert result is False
E assert None is False
tests/test_github_client.py:1977: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - assert None is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 0, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -102,7 +102,7 @@
try:
repo.complete()
except GithubException as e:
- return e.status == 404
+ return e.status <= 404
except Exception:
return False
return False........................................................................ [ 15%]
........................................................................ [ 31%]
............................F
=================================== FAILURES ===================================
_ TestRepositoryIsMissing.test_repository_is_missing_is_false_unless_github_says_404[outcome1] _
self = <tests.test_github_client.TestRepositoryIsMissing object at 0x7fda4cafbce0>
outcome = GithubException(403 {"message": "Forbidden"})
@pytest.mark.parametrize(
"outcome",
[
None,
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_repository_is_missing_is_false_unless_github_says_404(self, outcome):
"""Test success and every other failure keep the fail-closed path."""
from stampbot.github_client import _repository_is_missing
repo = Mock()
if outcome is not None:
repo.complete.side_effect = outcome
> assert _repository_is_missing(repo) is False
E AssertionError: assert True is False
E + where True = <function _repository_is_missing at 0x7fda49bc6560>(<Mock id='140575505959296'>)
tests/test_github_client.py:2317: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepositoryIsMissing::test_repository_is_missing_is_false_unless_github_says_404[outcome1] - AssertionError: assert True is False
+ where True = <function _repository_is_missing at 0x7fda49bc6560>(<Mock id='140575505959296'>)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 172 passed, 3 deselected, 1 warning in 2.95s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 1, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if e.status <= 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -710,7 +710,7 @@
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
- repository_is_optionally_missing = not_found == "repository"
+ repository_is_optionally_missing = not_found <= "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.25s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 3, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -711,7 +711,7 @@
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
- file_is_confirmed_missing = not_found == "file"
+ file_is_confirmed_missing = not_found <= "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 4, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -812,7 +812,7 @@
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
- if review.user.login == bot_user and review.state in (
+ if review.user.login <= bot_user and review.state in (
"APPROVED",
"DISMISSED",
):........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f0c37e88b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert [{'id': 123, ...0399217520'>}] == [{'id': 123, ...d': 'oldsha'}]
E
E Left contains one more item: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='139690399217520'>}
E
E Full diff:
E [
E {
E 'id': 123,
E 'state': 'APPROVED',
E 'commit_id': 'headsha',
E },
E {
E 'id': 124,
E 'state': 'DISMISSED',
E 'commit_id': 'oldsha',
E },
E + {
E + 'id': 456,
E + 'state': 'APPROVED',
E + 'commit_id': <Mock name='mock.commit_id' id='139690399217520'>,
E + },
E ]
tests/test_github_client.py:1481: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert [{'id': 123, ...0399217520'>}] == [{'id': 123, ...d': 'oldsha'}]
Left contains one more item: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='139690399217520'>}
Full diff:
[
{
'id': 123,
'state': 'APPROVED',
'commit_id': 'headsha',
},
{
'id': 124,
'state': 'DISMISSED',
'commit_id': 'oldsha',
},
+ {
+ 'id': 456,
+ 'state': 'APPROVED',
+ 'commit_id': <Mock name='mock.commit_id' id='139690399217520'>,
+ },
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.89s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 5, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1067,7 +1067,7 @@
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
- if e.status == 404:
+ if e.status <= 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 0, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -102,7 +102,7 @@
try:
repo.complete()
except GithubException as e:
- return e.status == 404
+ return e.status > 404
except Exception:
return False
return False........................................................................ [ 15%]
...................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_accepts_missing_optional_repository_with_lazy_client _
self = <tests.test_github_client.TestGetRepoFile object at 0x7fe1625c5400>
def test_get_repo_file_accepts_missing_optional_repository_with_lazy_client(self):
"""Test a lazy client confirms the optional repository itself is missing."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_integration_cls.return_value = mock_integration
# Lazy get_repo never raises. The file read and the root read both
# 404, and only the repository request proves the repo is absent.
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
GithubException(404, {"message": "Not Found"}, None),
]
mock_repo.complete.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/.github",
"stampbot.toml",
missing_repository_is_optional=True,
)
tests/test_github_client.py:1236:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140605936844880'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:48:48.481866Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/.github (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/.github (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:48:48.481866Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_accepts_missing_optional_repository_with_lazy_client - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 139 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 1, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if e.status > 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f4c88d3b100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139966631507152'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:39:18.377359Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:39:18.377359Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -710,7 +710,7 @@
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
- repository_is_optionally_missing = not_found == "repository"
+ repository_is_optionally_missing = not_found > "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()........................................................................ [ 15%]
.............................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_propagates_unconfirmed_not_found[root_result0] _
self = <tests.test_github_client.TestGetRepoFile object at 0x7fb105e9f230>
root_result = GithubException(404 {"message": "Not Found"})
@pytest.mark.parametrize(
"root_result",
[
GithubException(404, {"message": "Not Found"}, None),
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub root read timed out"),
Mock(),
],
)
def test_get_repo_file_propagates_unconfirmed_not_found(self, root_result):
"""Test a contents 404 fails closed unless the same ref remains readable."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo.get_contents.side_effect = [first, root_result]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(GithubException) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE GithubException
tests/test_github_client.py:1110: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_unconfirmed_not_found[root_result0] - Failed: DID NOT RAISE GithubException
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 133 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 3, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -711,7 +711,7 @@
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
- file_is_confirmed_missing = not_found == "file"
+ file_is_confirmed_missing = not_found > "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7fe5bb773100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140624542676176'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T23:04:19.466589Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T23:04:19.466589Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 4, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -812,7 +812,7 @@
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
- if review.user.login == bot_user and review.state in (
+ if review.user.login > bot_user and review.state in (
"APPROVED",
"DISMISSED",
):........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fbf6f9e8b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert [] == [{'id': 123, ...d': 'oldsha'}]
E
E Right contains 2 more items, first extra item: {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
E
E Full diff:
E + []
E - [
E - {
E - 'id': 123,
E - 'state': 'APPROVED',
E - 'commit_id': 'headsha',
E - },
E - {
E - 'id': 124,
E - 'state': 'DISMISSED',
E - 'commit_id': 'oldsha',
E - },
E - ]
tests/test_github_client.py:1481: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert [] == [{'id': 123, ...d': 'oldsha'}]
Right contains 2 more items, first extra item: {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
Full diff:
+ []
- [
- {
- 'id': 123,
- 'state': 'APPROVED',
- 'commit_id': 'headsha',
- },
- {
- 'id': 124,
- 'state': 'DISMISSED',
- 'commit_id': 'oldsha',
- },
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 5, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1067,7 +1067,7 @@
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
- if e.status == 404:
+ if e.status > 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f07a6e9d450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "missing")
> assert result is False
E assert None is False
tests/test_github_client.py:1977: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - assert None is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 2.91s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 0, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -102,7 +102,7 @@
try:
repo.complete()
except GithubException as e:
- return e.status == 404
+ return e.status >= 404
except Exception:
return False
return False........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 1, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if e.status >= 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -710,7 +710,7 @@
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
- repository_is_optionally_missing = not_found == "repository"
+ repository_is_optionally_missing = not_found >= "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()........................................................................ [ 15%]
.............................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_propagates_unconfirmed_not_found[root_result0] _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f8bdc98f230>
root_result = GithubException(404 {"message": "Not Found"})
@pytest.mark.parametrize(
"root_result",
[
GithubException(404, {"message": "Not Found"}, None),
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub root read timed out"),
Mock(),
],
)
def test_get_repo_file_propagates_unconfirmed_not_found(self, root_result):
"""Test a contents 404 fails closed unless the same ref remains readable."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo.get_contents.side_effect = [first, root_result]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(GithubException) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE GithubException
tests/test_github_client.py:1110: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_unconfirmed_not_found[root_result0] - Failed: DID NOT RAISE GithubException
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 133 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 3, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -711,7 +711,7 @@
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
- file_is_confirmed_missing = not_found == "file"
+ file_is_confirmed_missing = not_found >= "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
........................................................................ [ 15%]
.............................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_propagates_unconfirmed_not_found[root_result0] _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f55ae2eb230>
root_result = GithubException(404 {"message": "Not Found"})
@pytest.mark.parametrize(
"root_result",
[
GithubException(404, {"message": "Not Found"}, None),
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub root read timed out"),
Mock(),
],
)
def test_get_repo_file_propagates_unconfirmed_not_found(self, root_result):
"""Test a contents 404 fails closed unless the same ref remains readable."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo.get_contents.side_effect = [first, root_result]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(GithubException) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE GithubException
tests/test_github_client.py:1110: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_unconfirmed_not_found[root_result0] - Failed: DID NOT RAISE GithubException
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 133 passed, 3 deselected, 1 warning in 2.82s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 4, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -812,7 +812,7 @@
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
- if review.user.login == bot_user and review.state in (
+ if review.user.login >= bot_user and review.state in (
"APPROVED",
"DISMISSED",
):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 5, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1067,7 +1067,7 @@
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
- if e.status == 404:
+ if e.status >= 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False........................................................................ [ 15%]
........................................................................ [ 31%]
...............F
=================================== FAILURES ===================================
____________ TestRepoHasLabel.test_repo_has_label_github_exception _____________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fcde5e3f820>
def test_repo_has_label_github_exception(self):
"""Test repo_has_label returns None on GitHubException errors."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(500, {"message": "Error"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "autoapprove")
> assert result is None
E assert False is None
tests/test_github_client.py:2015: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_github_exception - assert False is None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 159 passed, 3 deselected, 1 warning in 2.93s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 0, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -710,7 +710,7 @@
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
- repository_is_optionally_missing = not_found == "repository"
+ repository_is_optionally_missing = not_found is "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/github_client.py:713
/home/runner/work/stampbot/stampbot/stampbot/github_client.py:713: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
repository_is_optionally_missing = not_found is "repository"
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 1, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -711,7 +711,7 @@
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
- file_is_confirmed_missing = not_found == "file"
+ file_is_confirmed_missing = not_found is "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/github_client.py:714
/home/runner/work/stampbot/stampbot/stampbot/github_client.py:714: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
file_is_confirmed_missing = not_found is "file"
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 2, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -812,7 +812,7 @@
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
- if review.user.login == bot_user and review.state in (
+ if review.user.login is bot_user and review.state in (
"APPROVED",
"DISMISSED",
):........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f13a6498b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert [] == [{'id': 123, ...d': 'oldsha'}]
E
E Right contains 2 more items, first extra item: {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
E
E Full diff:
E + []
E - [
E - {
E - 'id': 123,
E - 'state': 'APPROVED',
E - 'commit_id': 'headsha',
E - },
E - {
E - 'id': 124,
E - 'state': 'DISMISSED',
E - 'commit_id': 'oldsha',
E - },
E - ]
tests/test_github_client.py:1481: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert [] == [{'id': 123, ...d': 'oldsha'}]
Right contains 2 more items, first extra item: {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
Full diff:
+ []
- [
- {
- 'id': 123,
- 'state': 'APPROVED',
- 'commit_id': 'headsha',
- },
- {
- 'id': 124,
- 'state': 'DISMISSED',
- 'commit_id': 'oldsha',
- },
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 0, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -710,7 +710,7 @@
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
- repository_is_optionally_missing = not_found == "repository"
+ repository_is_optionally_missing = not_found is not "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()........................................................................ [ 15%]
.............................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_propagates_unconfirmed_not_found[root_result0] _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f24f12ef230>
root_result = GithubException(404 {"message": "Not Found"})
@pytest.mark.parametrize(
"root_result",
[
GithubException(404, {"message": "Not Found"}, None),
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub root read timed out"),
Mock(),
],
)
def test_get_repo_file_propagates_unconfirmed_not_found(self, root_result):
"""Test a contents 404 fails closed unless the same ref remains readable."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo.get_contents.side_effect = [first, root_result]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(GithubException) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE GithubException
tests/test_github_client.py:1110: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/github_client.py:713
/home/runner/work/stampbot/stampbot/stampbot/github_client.py:713: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
repository_is_optionally_missing = not_found is not "repository"
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_unconfirmed_not_found[root_result0] - Failed: DID NOT RAISE GithubException
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 133 passed, 3 deselected, 2 warnings in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 1, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -711,7 +711,7 @@
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
- file_is_confirmed_missing = not_found == "file"
+ file_is_confirmed_missing = not_found is not "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f2e91bab100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139837931730128'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:45:35.530255Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:45:35.530255Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/github_client.py:714
/home/runner/work/stampbot/stampbot/stampbot/github_client.py:714: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
file_is_confirmed_missing = not_found is not "file"
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 2 warnings in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 2, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -812,7 +812,7 @@
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
- if review.user.login == bot_user and review.state in (
+ if review.user.login is not bot_user and review.state in (
"APPROVED",
"DISMISSED",
):........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f9c81ca8b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert [{'id': 123, ...0112237424'>}] == [{'id': 123, ...d': 'oldsha'}]
E
E Left contains one more item: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='140310112237424'>}
E
E Full diff:
E [
E {
E 'id': 123,
E 'state': 'APPROVED',
E 'commit_id': 'headsha',
E },
E {
E 'id': 124,
E 'state': 'DISMISSED',
E 'commit_id': 'oldsha',
E },
E + {
E + 'id': 456,
E + 'state': 'APPROVED',
E + 'commit_id': <Mock name='mock.commit_id' id='140310112237424'>,
E + },
E ]
tests/test_github_client.py:1481: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert [{'id': 123, ...0112237424'>}] == [{'id': 123, ...d': 'oldsha'}]
Left contains one more item: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='140310112237424'>}
Full diff:
[
{
'id': 123,
'state': 'APPROVED',
'commit_id': 'headsha',
},
{
'id': 124,
'state': 'DISMISSED',
'commit_id': 'oldsha',
},
+ {
+ 'id': 456,
+ 'state': 'APPROVED',
+ 'commit_id': <Mock name='mock.commit_id' id='140310112237424'>,
+ },
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Eq, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[-1] == expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f649a1cb770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f64957ca8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] == expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Eq, occurrence: 1, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status == 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7fe3dcce47d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fe3d9a767b0>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:36:34.970829Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:36:34.970829Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fe3d9a767b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Eq, occurrence: 2, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 422 or pr.get_review(review_id).state == "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7faac30747d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7faabbd6e7b0>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:37:15.890970Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:37:15.890970Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7faabbd6e7b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 2.89s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_NotEq_Lt, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[-1] < expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.23s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_NotEq_Lt, occurrence: 1, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status < 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_NotEq_Lt, occurrence: 2, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 422 or pr.get_review(review_id).state < "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_LtE, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[-1] <= expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fd5db16f770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd5d65728b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] <= expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_LtE, occurrence: 1, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status <= 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7faa8f8407d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7faa8c732850>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T23:01:40.930455Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T23:01:40.930455Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7faa8c732850>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 3.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_LtE, occurrence: 2, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 422 or pr.get_review(review_id).state <= "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7fdf1003c7d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fdf0cf367b0>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:27:10.274385Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:27:10.274385Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fdf0cf367b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Gt, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[-1] > expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
..................................F
=================================== FAILURES ===================================
______ TestLoadPrivateKey.test_load_private_key_rejects_mismatched_footer ______
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f3039c29d60>
def test_load_private_key_rejects_mismatched_footer(self):
"""Test the PEM header and footer must describe the same key type."""
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = (
"-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----"
)
from stampbot.github_client import GitHubAppClient
> with pytest.raises(ValueError, match="footer does not match"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_github_client.py:271: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_rejects_mismatched_footer - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 106 passed, 3 deselected, 1 warning in 2.92s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_NotEq_Gt, occurrence: 1, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status > 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Gt, occurrence: 2, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 422 or pr.get_review(review_id).state > "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <tests.test_github_client.TestDismissApproval object at 0x7ff86b792ea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
E AssertionError: assert True is False
E + where True = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7ff86832a7b0>.dismiss_approval
tests/test_github_client.py:864: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:17:41.684585Z[0m [[32m[1minfo [0m] [1mReview 789 on PR #42 in owner/repo was already dismissed[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35minfo[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'review_id': 789}[0m
[2m2026-09-04T20:17:41.684830Z[0m [[32m[1minfo [0m] [1mDismissed approval for PR #42 in owner/repo[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35minfo[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'review_id': 789}[0m
------------------------------ Captured log call -------------------------------
INFO stampbot.github_client:github_client.py:590 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'review_id': 789}, 'event': 'Review 789 on PR #42 in owner/repo was already dismissed', 'logger': 'stampbot.github_client', 'level': 'info', 'timestamp': '2026-09-04T20:17:41.684585Z'}
INFO stampbot.github_client:github_client.py:611 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'review_id': 789}, 'event': 'Dismissed approval for PR #42 in owner/repo', 'logger': 'stampbot.github_client', 'level': 'info', 'timestamp': '2026-09-04T20:17:41.684830Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - AssertionError: assert True is False
+ where True = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7ff86832a7b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_GtE, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[-1] >= expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f2a677e7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f2a62ea28b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] >= expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_GtE, occurrence: 1, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status >= 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7fa4970c87d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fa48fd967b0>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:54:58.997444Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:54:58.997444Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fa48fd967b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_GtE, occurrence: 2, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 422 or pr.get_review(review_id).state >= "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7f61b48dc7d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f61b169e7b0>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:45:18.435958Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:45:18.435958Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f61b169e7b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Is, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[-1] is expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
..................................F
=================================== FAILURES ===================================
______ TestLoadPrivateKey.test_load_private_key_rejects_mismatched_footer ______
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7fa78ff45d60>
def test_load_private_key_rejects_mismatched_footer(self):
"""Test the PEM header and footer must describe the same key type."""
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = (
"-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----"
)
from stampbot.github_client import GitHubAppClient
> with pytest.raises(ValueError, match="footer does not match"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_github_client.py:271: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_rejects_mismatched_footer - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 106 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Is, occurrence: 1, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 422 or pr.get_review(review_id).state is "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7fd0940147d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fd090e5e7b0>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:20:18.400317Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:20:18.400317Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/github_client.py:588
/home/runner/work/stampbot/stampbot/stampbot/github_client.py:588: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if e.status != 422 or pr.get_review(review_id).state is "DISMISSED":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fd090e5e7b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 2 warnings in 2.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_IsNot, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[-1] is not expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fa8e6eef770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa8e25b28b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] is not expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.94s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_NotEq_IsNot, occurrence: 1, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 422 or pr.get_review(review_id).state is not "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/github_client.py:588
/home/runner/work/stampbot/stampbot/stampbot/github_client.py:588: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if e.status != 422 or pr.get_review(review_id).state is not "DISMISSED":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_Eq, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) == 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f5766bbf770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f57621ca8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) == 3 or not pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.78s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Lt_Eq, occurrence: 1, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining == 0 or limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Lt_Eq, occurrence: 2, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < 0 or limit == 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Lt_NotEq, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) != 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_NotEq, occurrence: 1, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining != 0 or limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7ff994fcfc50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
> assert client._rate_limit_installations == {"9001", "9002"}
E AssertionError: assert set() == {'9001', '9002'}
E
E Extra items in the right set:
E '9001'
E '9002'
E
E Full diff:
E + set()
E - {
E - '9001',
E - '9002',
E - }
tests/test_github_client.py:519: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert set() == {'9001', '9002'}
Extra items in the right set:
'9001'
'9002'
Full diff:
+ set()
- {
- '9001',
- '9002',
- }
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_NotEq, occurrence: 2, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < 0 or limit != 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7f95441d3c50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
> assert client._rate_limit_installations == {"9001", "9002"}
E AssertionError: assert set() == {'9001', '9002'}
E
E Extra items in the right set:
E '9001'
E '9002'
E
E Full diff:
E + set()
E - {
E - '9001',
E - '9002',
E - }
tests/test_github_client.py:519: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert set() == {'9001', '9002'}
Extra items in the right set:
'9001'
'9002'
Full diff:
+ set()
- {
- '9001',
- '9002',
- }
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_LtE, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) <= 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f07429e7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f073e0b28b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) <= 3 or not pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.85s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Lt_LtE, occurrence: 1, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining <= 0 or limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.23s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Lt_LtE, occurrence: 2, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < 0 or limit <= 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Lt_Gt, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) > 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_Gt, occurrence: 1, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining > 0 or limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7f8b11ef7c50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
> assert client._rate_limit_installations == {"9001", "9002"}
E AssertionError: assert set() == {'9001', '9002'}
E
E Extra items in the right set:
E '9002'
E '9001'
E
E Full diff:
E + set()
E - {
E - '9001',
E - '9002',
E - }
tests/test_github_client.py:519: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert set() == {'9001', '9002'}
Extra items in the right set:
'9002'
'9001'
Full diff:
+ set()
- {
- '9001',
- '9002',
- }
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_Gt, occurrence: 2, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < 0 or limit > 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7fb098dd7c50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
> assert client._rate_limit_installations == {"9001", "9002"}
E AssertionError: assert set() == {'9001', '9002'}
E
E Extra items in the right set:
E '9001'
E '9002'
E
E Full diff:
E + set()
E - {
E - '9001',
E - '9002',
E - }
tests/test_github_client.py:519: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert set() == {'9001', '9002'}
Extra items in the right set:
'9001'
'9002'
Full diff:
+ set()
- {
- '9001',
- '9002',
- }
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_GtE, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) >= 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f50250db770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f502079e8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) >= 3 or not pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_GtE, occurrence: 1, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining >= 0 or limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7f1c953e7c50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
> assert client._rate_limit_installations == {"9001", "9002"}
E AssertionError: assert set() == {'9001', '9002'}
E
E Extra items in the right set:
E '9002'
E '9001'
E
E Full diff:
E + set()
E - {
E - '9001',
E - '9002',
E - }
tests/test_github_client.py:519: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert set() == {'9001', '9002'}
Extra items in the right set:
'9002'
'9001'
Full diff:
+ set()
- {
- '9001',
- '9002',
- }
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_GtE, occurrence: 2, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < 0 or limit >= 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7f6ba355fc50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
> assert client._rate_limit_installations == {"9001", "9002"}
E AssertionError: assert set() == {'9001', '9002'}
E
E Extra items in the right set:
E '9002'
E '9001'
E
E Full diff:
E + set()
E - {
E - '9001',
E - '9002',
E - }
tests/test_github_client.py:519: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert set() == {'9001', '9002'}
Extra items in the right set:
'9002'
'9001'
Full diff:
+ set()
- {
- '9001',
- '9002',
- }
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.84s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -236,7 +236,7 @@
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
- if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
+ if file_metadata.st_size == MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -245,7 +245,7 @@
logger.error("Failed to read private key file: %s", e)
raise
- if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
+ if len(pem_content.encode("utf-8")) == MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the........................................................................ [ 15%]
...................................F
=================================== FAILURES ===================================
___ TestLoadPrivateKey.test_load_private_key_rejects_oversized_inline_value ____
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f6367771f40>
def test_load_private_key_rejects_oversized_inline_value(self):
"""Test an oversized inline value is rejected before PEM parsing."""
from stampbot.github_client import MAX_PRIVATE_KEY_SIZE, GitHubAppClient
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = TEST_PEM_KEY.replace("test", "x" * MAX_PRIVATE_KEY_SIZE)
> with pytest.raises(ValueError, match="exceeds"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_github_client.py:281: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_rejects_oversized_inline_value - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 107 passed, 3 deselected, 1 warning in 2.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 2, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None and self._bot_login_value[1] == now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f243077cb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
# The App slug is read once and reused for later review scans, and
# each operation builds exactly one client on the shared credentials.
client.find_bot_approval_reviews(123456, "owner/repo", 43)
> mock_integration.get_app.assert_called_once_with()
tests/test_github_client.py:1489:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='GithubIntegration().get_app' id='139793353627632'>, args = ()
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_app' to be called once. Called 2 times.
E Calls: [call(), call()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: Expected 'get_app' to be called once. Called 2 times.
Calls: [call(), call()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -236,7 +236,7 @@
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
- if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
+ if file_metadata.st_size != MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f745fc52780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-315/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f745cbba300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size != MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key file exceeds 65536 bytes
stampbot/github_client.py:240: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - ValueError: Private key file exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -245,7 +245,7 @@
logger.error("Failed to read private key file: %s", e)
raise
- if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
+ if len(pem_content.encode("utf-8")) != MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f6446d7f770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f64423a68b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) != MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 65536 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.77s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 2, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None and self._bot_login_value[1] != now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -236,7 +236,7 @@
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
- if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
+ if file_metadata.st_size < MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f100d61a780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-227/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f100a5fa300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size < MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key file exceeds 65536 bytes
stampbot/github_client.py:240: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - ValueError: Private key file exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.81s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -245,7 +245,7 @@
logger.error("Failed to read private key file: %s", e)
raise
- if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
+ if len(pem_content.encode("utf-8")) < MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f5cc3d4b770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f5cbf38e8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) < MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 65536 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 2, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None and self._bot_login_value[1] < now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f565f138b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
# The App slug is read once and reused for later review scans, and
# each operation builds exactly one client on the shared credentials.
client.find_bot_approval_reviews(123456, "owner/repo", 43)
> mock_integration.get_app.assert_called_once_with()
tests/test_github_client.py:1489:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='GithubIntegration().get_app' id='140008814614512'>, args = ()
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_app' to be called once. Called 2 times.
E Calls: [call(), call()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: Expected 'get_app' to be called once. Called 2 times.
Calls: [call(), call()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -236,7 +236,7 @@
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
- if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
+ if file_metadata.st_size <= MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f0fc54e6780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-1077/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f0fc24b2300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size <= MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key file exceeds 65536 bytes
stampbot/github_client.py:240: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - ValueError: Private key file exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -245,7 +245,7 @@
logger.error("Failed to read private key file: %s", e)
raise
- if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
+ if len(pem_content.encode("utf-8")) <= MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fbc015e7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fbbfcc568b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) <= MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 65536 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 2, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None and self._bot_login_value[1] <= now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fca41604b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
# The App slug is read once and reused for later review scans, and
# each operation builds exactly one client on the shared credentials.
client.find_bot_approval_reviews(123456, "owner/repo", 43)
> mock_integration.get_app.assert_called_once_with()
tests/test_github_client.py:1489:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='GithubIntegration().get_app' id='140506601265136'>, args = ()
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_app' to be called once. Called 2 times.
E Calls: [call(), call()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: Expected 'get_app' to be called once. Called 2 times.
Calls: [call(), call()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.91s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -236,7 +236,7 @@
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
- if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
+ if file_metadata.st_size >= MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -245,7 +245,7 @@
logger.error("Failed to read private key file: %s", e)
raise
- if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
+ if len(pem_content.encode("utf-8")) >= MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.14s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 2, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None and self._bot_login_value[1] >= now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -236,7 +236,7 @@
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
- if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
+ if file_metadata.st_size is MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -245,7 +245,7 @@
logger.error("Failed to read private key file: %s", e)
raise
- if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
+ if len(pem_content.encode("utf-8")) is MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the........................................................................ [ 15%]
...................................F
=================================== FAILURES ===================================
___ TestLoadPrivateKey.test_load_private_key_rejects_oversized_inline_value ____
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f67bc8a1f40>
def test_load_private_key_rejects_oversized_inline_value(self):
"""Test an oversized inline value is rejected before PEM parsing."""
from stampbot.github_client import MAX_PRIVATE_KEY_SIZE, GitHubAppClient
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = TEST_PEM_KEY.replace("test", "x" * MAX_PRIVATE_KEY_SIZE)
> with pytest.raises(ValueError, match="exceeds"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_github_client.py:281: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_rejects_oversized_inline_value - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 107 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 2, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None and self._bot_login_value[1] is now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fcb043ecb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
# The App slug is read once and reused for later review scans, and
# each operation builds exactly one client on the shared credentials.
client.find_bot_approval_reviews(123456, "owner/repo", 43)
> mock_integration.get_app.assert_called_once_with()
tests/test_github_client.py:1489:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='GithubIntegration().get_app' id='140509871052784'>, args = ()
kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'get_app' to be called once. Called 2 times.
E Calls: [call(), call()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: Expected 'get_app' to be called once. Called 2 times.
Calls: [call(), call()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -236,7 +236,7 @@
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
- if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
+ if file_metadata.st_size is not MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f7238132780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-1435/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f7235002300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size is not MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key file exceeds 65536 bytes
stampbot/github_client.py:240: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - ValueError: Private key file exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -245,7 +245,7 @@
logger.error("Failed to read private key file: %s", e)
raise
- if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
+ if len(pem_content.encode("utf-8")) is not MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f7066cb7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f70623ae8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) is not MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 65536 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.83s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 2, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None and self._bot_login_value[1] is not now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_GtE_Eq, occurrence: 0, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1149,7 +1149,7 @@
permission_index = -1
required_index = 99
- has_permission = permission_index >= required_index
+ has_permission = permission_index == required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_GtE_NotEq, occurrence: 0, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1149,7 +1149,7 @@
permission_index = -1
required_index = 99
- has_permission = permission_index >= required_index
+ has_permission = permission_index != required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7f571d438e10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_GtE_Lt, occurrence: 0, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1149,7 +1149,7 @@
permission_index = -1
required_index = 99
- has_permission = permission_index >= required_index
+ has_permission = permission_index < required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7f9f32dece10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_GtE_LtE, occurrence: 0, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1149,7 +1149,7 @@
permission_index = -1
required_index = 99
- has_permission = permission_index >= required_index
+ has_permission = permission_index <= required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(........................................................................ [ 15%]
........................................................................ [ 31%]
.....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_false _____________
self = <tests.test_github_client.TestUserHasPermission object at 0x7fe494964f50>
def test_user_has_permission_false(self):
"""Test insufficient permission returns False."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "read"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "bob", "write")
> assert result is False
E assert True is False
tests/test_github_client.py:1603: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_false - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 149 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_GtE_Gt, occurrence: 0, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1149,7 +1149,7 @@
permission_index = -1
required_index = 99
- has_permission = permission_index >= required_index
+ has_permission = permission_index > required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7f6c1b7a0e10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 2.93s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_GtE_Is, occurrence: 0, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1149,7 +1149,7 @@
permission_index = -1
required_index = 99
- has_permission = permission_index >= required_index
+ has_permission = permission_index is required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_GtE_IsNot, occurrence: 0, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1149,7 +1149,7 @@
permission_index = -1
required_index = 99
- has_permission = permission_index >= required_index
+ has_permission = permission_index is not required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(........................................................................ [ 15%]
........................................................................ [ 31%]
....F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_true ______________
self = <tests.test_github_client.TestUserHasPermission object at 0x7f0996824e10>
def test_user_has_permission_true(self):
"""Test required permission satisfied returns True."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.return_value = "write"
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "alice", "write")
> assert result is True
E assert False is True
tests/test_github_client.py:1566: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 148 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 0, definition_name: _get_repo_contents
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -78,7 +78,7 @@
ref: str | None,
) -> list[ContentFile] | ContentFile:
"""Call PyGithub without passing ``None`` as an explicit Git ref."""
- if ref is None:
+ if ref is not None:
return repo.get_contents(path)
return repo.get_contents(path, ref=ref)
........................................................................ [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7effc5bb8a50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
assert result == "file content"
> mock_repo.get_contents.assert_called_once_with("stampbot.toml", ref="main")
tests/test_github_client.py:986:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139636941334112'>
args = ('stampbot.toml',), kwargs = {'ref': 'main'}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: get_contents('stampbot.toml', ref='main')
E Actual: get_contents('stampbot.toml')
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - AssertionError: expected call not found.
Expected: get_contents('stampbot.toml', ref='main')
Actual: get_contents('stampbot.toml')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 1, definition_name: _ensure_initialized
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -182,7 +182,7 @@
# is_configured() guarantees app_id is set, but check for type safety
app_id = settings.app_id
- if app_id is None:
+ if app_id is not None:
raise RuntimeError("App ID not configured")
private_key = self._load_private_key()........................................................................ [ 15%]
......................F
=================================== FAILURES ===================================
____ TestEnsureInitialized.test_ensure_initialized_raises_when_app_id_none _____
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f82866ddcd0>
def test_ensure_initialized_raises_when_app_id_none(self):
"""Test that _ensure_initialized raises when app_id is None."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
):
mock_settings.app_id = None
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(RuntimeError, match="App ID not configured"):
> client._ensure_initialized()
tests/test_github_client.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f82836c7e30>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_raises_when_app_id_none - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 94 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 2, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -221,7 +221,7 @@
"""
key: str | None = settings.private_key
- if key is None:
+ if key is not None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fbebe957770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fbeb9f9a8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is not None:
> raise RuntimeError("Private key not configured")
E RuntimeError: Private key not configured
stampbot/github_client.py:225: RuntimeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - RuntimeError: Private key not configured
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 3, definition_name: _installation_auth
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -303,7 +303,7 @@
"""
with self._installation_auths_lock:
installation_auth = self._installation_auths.get(installation_id)
- if installation_auth is None:
+ if installation_auth is not None:
# The App credentials must be loaded before an installation auth
# can be derived from them.
self._ensure_initialized()........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7f606b94f9d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140052046267792'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f606861bf70>
installation_id = 123456
def _get_installation_client(self, installation_id: int) -> Github:
"""Get an authenticated GitHub client for an installation.
Every call builds a private lazy client on credentials shared per
installation, so the token is reused while connection state stays
private to the calling thread. Binding the client gives the credentials
the requester PyGithub needs for an exchange. The token is then read
here, before any repository request, so the first exchange or a due
refresh happens now and an authentication failure never looks like a
missing repository. A failed exchange leaves the credentials in place;
the next read retries.
Args:
installation_id: GitHub App installation ID.
Returns:
Authenticated Github client instance with timeout and retry configured.
Raises:
github.GithubException: If token exchange fails.
"""
installation_auth = self._installation_auth(installation_id)
client = self._new_client(installation_auth)
> installation_auth.token # noqa: B018
^^^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'token'
stampbot/github_client.py:354: AttributeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - AttributeError: 'NoneType' object has no attribute 'token'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 0, definition_name: integration
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -205,7 +205,7 @@
RuntimeError: If GitHub App credentials are not configured.
"""
self._ensure_initialized()
- assert self._integration is not None # guaranteed by _ensure_initialized
+ assert self._integration is None # guaranteed by _ensure_initialized
return self._integration
def _load_private_key(self) -> str:........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fbc0363cb90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:37:43.590862Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: [0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': ''}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': ''}, 'event': 'Failed to find bot approval reviews for PR #42 in owner/repo: ', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:37:43.590862Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 1, definition_name: _installation_auth
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -307,7 +307,7 @@
# The App credentials must be loaded before an installation auth
# can be derived from them.
self._ensure_initialized()
- assert self._auth is not None # guaranteed by _ensure_initialized
+ assert self._auth is None # guaranteed by _ensure_initialized
installation_auth = InstrumentedInstallationAuth(self._auth, installation_id)
self._installation_auths[installation_id] = installation_auth
self._prune_rate_limit_metrics()........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7fdf3ffcb9d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140596774929808'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fdf3cc07f70>
installation_id = 123456
def _installation_auth(self, installation_id: int) -> InstrumentedInstallationAuth:
"""Return the shared credentials for an installation, creating them on first use.
No network request happens here, so the cache lock is held only briefly.
The token exchange happens when a caller reads ``token``, serialized per
installation by the credentials themselves.
Args:
installation_id: GitHub App installation ID.
Returns:
Credentials whose token PyGithub exchanges on first read and
refreshes before GitHub expires it.
Raises:
RuntimeError: If the GitHub App credentials are not configured.
"""
with self._installation_auths_lock:
installation_auth = self._installation_auths.get(installation_id)
if installation_auth is None:
# The App credentials must be loaded before an installation auth
# can be derived from them.
self._ensure_initialized()
> assert self._auth is None # guaranteed by _ensure_initialized
^^^^^^^^^^^^^^^^^^
E AssertionError
stampbot/github_client.py:310: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - AssertionError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 2, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f47c7430b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:02:47.523733Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: 'NoneType' object is not subscriptable[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object is not subscriptable"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object is not subscriptable"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: 'NoneType' object is not subscriptable", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:02:47.523733Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 3, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if e.status == 404 and repo is None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f701554b100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140119312544976'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:29:48.951314Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:29:48.951314Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_UAdd, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[+1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f2efcf5f770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f2ef85a28b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[+1] != expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_UAdd, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if not any(line.strip() for line in pem_lines[1:+1]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fde7ceb7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fde7857a8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:+1]):
> raise ValueError("Private key PEM body is empty")
E ValueError: Private key PEM body is empty
stampbot/github_client.py:262: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM body is empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.92s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceUnaryOperator_USub_UAdd, occurrence: 2, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1146,7 +1146,7 @@
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
- permission_index = -1
+ permission_index = +1
required_index = 99
has_permission = permission_index >= required_index........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.16s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceUnaryOperator_USub_UAdd, occurrence: 3, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1231,7 +1231,7 @@
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
- team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
+ team_slug = team_ref.split("/")[+1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Invert, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[~1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f45d9e4b770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f45d52b28b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[~1] != expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Invert, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if not any(line.strip() for line in pem_lines[1:~1]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f8bc61bf770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f8bc188a8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:~1]):
> raise ValueError("Private key PEM body is empty")
E ValueError: Private key PEM body is empty
stampbot/github_client.py:262: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM body is empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.92s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceUnaryOperator_USub_Invert, occurrence: 2, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1146,7 +1146,7 @@
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
- permission_index = -1
+ permission_index = ~1
required_index = 99
has_permission = permission_index >= required_index........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Invert, occurrence: 3, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1231,7 +1231,7 @@
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
- team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
+ team_slug = team_ref.split("/")[~1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)........................................................................ [ 15%]
........................................................................ [ 31%]
.........F
=================================== FAILURES ===================================
________ TestGetUserTeamSlugs.test_get_user_team_slugs_with_org_prefix _________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7ff97b2411d0>
def test_get_user_team_slugs_with_org_prefix(self):
"""Test team membership check with org/team format."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_user = Mock()
mock_team = Mock()
mock_team.has_in_members.return_value = True
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(123456, "acme", "alice", ["acme/release-team"])
# Should extract "release-team" from "acme/release-team"
> mock_org.get_team_by_slug.assert_called_with("release-team")
tests/test_github_client.py:1775:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization().get_team_by_slug' id='140709366515088'>
args = ('release-team',), kwargs = {}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: get_team_by_slug('release-team')
E Actual: get_team_by_slug('acme')
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_with_org_prefix - AssertionError: expected call not found.
Expected: get_team_by_slug('release-team')
Actual: get_team_by_slug('acme')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 153 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Not, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[not 1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fa9584a3770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa953baa8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[not 1] != expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Not, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if not any(line.strip() for line in pem_lines[1:not 1]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fa1b3acf770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa1af19e8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:not 1]):
> raise ValueError("Private key PEM body is empty")
E ValueError: Private key PEM body is empty
stampbot/github_client.py:262: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM body is empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.79s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceUnaryOperator_USub_Not, occurrence: 2, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1146,7 +1146,7 @@
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
- permission_index = -1
+ permission_index = not 1
required_index = 99
has_permission = permission_index >= required_index........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Not, occurrence: 3, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1231,7 +1231,7 @@
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
- team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
+ team_slug = team_ref.split("/")[not 1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)........................................................................ [ 15%]
........................................................................ [ 31%]
.........F
=================================== FAILURES ===================================
________ TestGetUserTeamSlugs.test_get_user_team_slugs_with_org_prefix _________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f044e0c91d0>
def test_get_user_team_slugs_with_org_prefix(self):
"""Test team membership check with org/team format."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_user = Mock()
mock_team = Mock()
mock_team.has_in_members.return_value = True
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(123456, "acme", "alice", ["acme/release-team"])
# Should extract "release-team" from "acme/release-team"
> mock_org.get_team_by_slug.assert_called_with("release-team")
tests/test_github_client.py:1775:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization().get_team_by_slug' id='139656410170768'>
args = ('release-team',), kwargs = {}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: get_team_by_slug('release-team')
E Actual: get_team_by_slug('acme')
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_with_org_prefix - AssertionError: expected call not found.
Expected: get_team_by_slug('release-team')
Actual: get_team_by_slug('acme')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 153 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_USub, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f43d9dd7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f43d548a8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[1] != expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_USub, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if not any(line.strip() for line in pem_lines[1:1]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fb0dddaf770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb0d93be8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:1]):
> raise ValueError("Private key PEM body is empty")
E ValueError: Private key PEM body is empty
stampbot/github_client.py:262: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM body is empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.77s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceUnaryOperator_Delete_USub, occurrence: 2, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1146,7 +1146,7 @@
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
- permission_index = -1
+ permission_index = 1
required_index = 99
has_permission = permission_index >= required_index........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceUnaryOperator_Delete_USub, occurrence: 3, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1231,7 +1231,7 @@
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
- team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
+ team_slug = team_ref.split("/")[1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 0, definition_name: _ensure_initialized
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -175,7 +175,7 @@
if self._initialized:
return
- if not is_configured():
+ if is_configured():
raise RuntimeError(
"GitHub App not configured. Visit /setup to create your GitHub App."
)........................................................................ [ 15%]
.....................F
=================================== FAILURES ===================================
___ TestEnsureInitialized.test_ensure_initialized_raises_when_not_configured ___
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f2890c6de00>
def test_ensure_initialized_raises_when_not_configured(self):
"""Test that _ensure_initialized raises when app not configured."""
with patch("stampbot.github_client.is_configured", return_value=False):
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(RuntimeError, match="GitHub App not configured"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE RuntimeError
tests/test_github_client.py:83: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_raises_when_not_configured - Failed: DID NOT RAISE RuntimeError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 93 passed, 3 deselected, 1 warning in 2.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 1, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -234,7 +234,7 @@
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
- if not stat.S_ISREG(file_metadata.st_mode):
+ if stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f36c5636780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-1441/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f36c2502300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if stat.S_ISREG(file_metadata.st_mode):
> raise ValueError("Private key path must reference a regular file")
E ValueError: Private key path must reference a regular file
stampbot/github_client.py:238: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - ValueError: Private key path must reference a regular file
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 2, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) < 3 or pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fba04d5b770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fba003be8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 3, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -253,7 +253,7 @@
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
- if not pem_lines[0].endswith("PRIVATE KEY-----"):
+ if pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fda3fecf770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fda3b57e8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if pem_lines[0].endswith("PRIVATE KEY-----"):
> raise ValueError("PEM value must contain a private key")
E ValueError: PEM value must contain a private key
stampbot/github_client.py:257: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: PEM value must contain a private key
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 4, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fa798f77770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa79458a8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if any(line.strip() for line in pem_lines[1:-1]):
> raise ValueError("Private key PEM body is empty")
E ValueError: Private key PEM body is empty
stampbot/github_client.py:262: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM body is empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 0, definition_name: _get_repo_contents
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -78,7 +78,7 @@
ref: str | None,
) -> list[ContentFile] | ContentFile:
"""Call PyGithub without passing ``None`` as an explicit Git ref."""
- if ref is None:
+ if not ref is None:
return repo.get_contents(path)
return repo.get_contents(path, ref=ref)
........................................................................ [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f7305534a50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
assert result == "file content"
> mock_repo.get_contents.assert_called_once_with("stampbot.toml", ref="main")
tests/test_github_client.py:986:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140131930203744'>
args = ('stampbot.toml',), kwargs = {'ref': 'main'}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: get_contents('stampbot.toml', ref='main')
E Actual: get_contents('stampbot.toml')
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - AssertionError: expected call not found.
Expected: get_contents('stampbot.toml', ref='main')
Actual: get_contents('stampbot.toml')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 1, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -119,7 +119,7 @@
``.github`` repository. A readable repository root proves the file is
absent. Anything else stays unconfirmed and the caller fails closed.
"""
- if optional_repo and _repository_is_missing(repo):
+ if not optional_repo and _repository_is_missing(repo):
return "repository"
if _can_read_repo_root(repo, ref):
return "file"........................................................................ [ 15%]
...................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_accepts_missing_optional_repository_with_lazy_client _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f3d1c865400>
def test_get_repo_file_accepts_missing_optional_repository_with_lazy_client(self):
"""Test a lazy client confirms the optional repository itself is missing."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_integration_cls.return_value = mock_integration
# Lazy get_repo never raises. The file read and the root read both
# 404, and only the repository request proves the repo is absent.
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
GithubException(404, {"message": "Not Found"}, None),
]
mock_repo.complete.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/.github",
"stampbot.toml",
missing_repository_is_optional=True,
)
tests/test_github_client.py:1236:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139900390873408'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:21:19.495994Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/.github (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/.github (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:21:19.495994Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_accepts_missing_optional_repository_with_lazy_client - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 139 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 2, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -121,7 +121,7 @@
"""
if optional_repo and _repository_is_missing(repo):
return "repository"
- if _can_read_repo_root(repo, ref):
+ if not _can_read_repo_root(repo, ref):
return "file"
return "unconfirmed"
........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f882541f100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140222659376336'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:28:05.407020Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:28:05.407020Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 3, definition_name: _ensure_initialized
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -168,7 +168,7 @@
Raises:
RuntimeError: If GitHub App credentials are not configured.
"""
- if self._initialized:
+ if not self._initialized:
return
with self._init_lock:........................................................................ [ 15%]
.....................F
=================================== FAILURES ===================================
___ TestEnsureInitialized.test_ensure_initialized_raises_when_not_configured ___
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f4433979e00>
def test_ensure_initialized_raises_when_not_configured(self):
"""Test that _ensure_initialized raises when app not configured."""
with patch("stampbot.github_client.is_configured", return_value=False):
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(RuntimeError, match="GitHub App not configured"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE RuntimeError
tests/test_github_client.py:83: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_raises_when_not_configured - Failed: DID NOT RAISE RuntimeError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 93 passed, 3 deselected, 1 warning in 2.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 4, definition_name: _ensure_initialized
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -172,7 +172,7 @@
return
with self._init_lock:
- if self._initialized:
+ if not self._initialized:
return
if not is_configured():........................................................................ [ 15%]
....................F
=================================== FAILURES ===================================
______ TestEnsureInitialized.test_ensure_initialized_rechecks_under_lock _______
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f751bdcb4d0>
def test_ensure_initialized_rechecks_under_lock(self):
"""Test a thread that loses the initialization race does not build credentials again."""
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
class LockThatLosesTheRace:
def __enter__(self):
# Another thread finished initializing while this one waited.
client._initialized = True
def __exit__(self, *exc_info):
return False
client._init_lock = LockThatLosesTheRace() # type: ignore[assignment]
with patch("stampbot.github_client.is_configured") as mock_is_configured:
client._ensure_initialized()
> mock_is_configured.assert_not_called()
tests/test_github_client.py:74:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='is_configured' id='140140901396368'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'is_configured' to not have been called. Called 1 times.
E Calls: [call(), call().__bool__()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_rechecks_under_lock - AssertionError: Expected 'is_configured' to not have been called. Called 1 times.
Calls: [call(), call().__bool__()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 92 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 5, definition_name: _ensure_initialized
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -175,7 +175,7 @@
if self._initialized:
return
- if not is_configured():
+ if not not is_configured():
raise RuntimeError(
"GitHub App not configured. Visit /setup to create your GitHub App."
)........................................................................ [ 15%]
.....................F
=================================== FAILURES ===================================
___ TestEnsureInitialized.test_ensure_initialized_raises_when_not_configured ___
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fd7b6591e00>
def test_ensure_initialized_raises_when_not_configured(self):
"""Test that _ensure_initialized raises when app not configured."""
with patch("stampbot.github_client.is_configured", return_value=False):
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(RuntimeError, match="GitHub App not configured"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE RuntimeError
tests/test_github_client.py:83: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_raises_when_not_configured - Failed: DID NOT RAISE RuntimeError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 93 passed, 3 deselected, 1 warning in 2.81s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 6, definition_name: _ensure_initialized
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -182,7 +182,7 @@
# is_configured() guarantees app_id is set, but check for type safety
app_id = settings.app_id
- if app_id is None:
+ if not app_id is None:
raise RuntimeError("App ID not configured")
private_key = self._load_private_key()........................................................................ [ 15%]
......................F
=================================== FAILURES ===================================
____ TestEnsureInitialized.test_ensure_initialized_raises_when_app_id_none _____
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f2c835a5cd0>
def test_ensure_initialized_raises_when_app_id_none(self):
"""Test that _ensure_initialized raises when app_id is None."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
):
mock_settings.app_id = None
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(RuntimeError, match="App ID not configured"):
> client._ensure_initialized()
tests/test_github_client.py:98:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f2c804d7e30>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_raises_when_app_id_none - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 94 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 7, definition_name: integration
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -205,7 +205,7 @@
RuntimeError: If GitHub App credentials are not configured.
"""
self._ensure_initialized()
- assert self._integration is not None # guaranteed by _ensure_initialized
+ assert not self._integration is not None # guaranteed by _ensure_initialized
return self._integration
def _load_private_key(self) -> str:........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fd7a0684b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:48:53.523823Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: [0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': ''}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': ''}, 'event': 'Failed to find bot approval reviews for PR #42 in owner/repo: ', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:48:53.523823Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 8, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -221,7 +221,7 @@
"""
key: str | None = settings.private_key
- if key is None:
+ if not key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fcaa433f770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fca9f9968b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if not key is None:
> raise RuntimeError("Private key not configured")
E RuntimeError: Private key not configured
stampbot/github_client.py:225: RuntimeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - RuntimeError: Private key not configured
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 9, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -228,7 +228,7 @@
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
- if key.startswith("-----BEGIN"):
+ if not key.startswith("-----BEGIN"):
pem_content = key
else:
try:........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fc550db3770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fc54c3aa8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if not key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
> raise ValueError("Invalid private key path: file does not exist") from None
E ValueError: Invalid private key path: file does not exist
stampbot/github_client.py:243: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Invalid private key path: file does not exist
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.81s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 10, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -234,7 +234,7 @@
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
- if not stat.S_ISREG(file_metadata.st_mode):
+ if not not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f322af42780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-1417/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f3223d4a300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not not stat.S_ISREG(file_metadata.st_mode):
> raise ValueError("Private key path must reference a regular file")
E ValueError: Private key path must reference a regular file
stampbot/github_client.py:238: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - ValueError: Private key path must reference a regular file
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 11, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -236,7 +236,7 @@
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
- if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
+ if not file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:........................................................................ [ 15%]
...........................F
=================================== FAILURES ===================================
___________ TestLoadPrivateKey.test_load_private_key_reads_from_file ___________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7fc15c156780>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-1103/test_load_private_key_reads_fr0')
def test_load_private_key_reads_from_file(self, tmp_path):
"""Test that private key is read from file when path provided."""
pem_key = TEST_PEM_KEY
key_file = tmp_path / "private-key.pem"
key_file.write_text(pem_key)
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:172:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fc15903e300>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if not file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key file exceeds 65536 bytes
stampbot/github_client.py:240: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_reads_from_file - ValueError: Private key file exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 99 passed, 3 deselected, 1 warning in 2.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 12, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -245,7 +245,7 @@
logger.error("Failed to read private key file: %s", e)
raise
- if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
+ if not len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f9e78f37770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f9e745aa8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if not len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
> raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
E ValueError: Private key exceeds 65536 bytes
stampbot/github_client.py:249: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key exceeds 65536 bytes
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 13, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if not len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f3e20dcf770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f3e1c4aa8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if not len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 14, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -253,7 +253,7 @@
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
- if not pem_lines[0].endswith("PRIVATE KEY-----"):
+ if not not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f6fd253b770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f6fcdcfa8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not not pem_lines[0].endswith("PRIVATE KEY-----"):
> raise ValueError("PEM value must contain a private key")
E ValueError: PEM value must contain a private key
stampbot/github_client.py:257: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: PEM value must contain a private key
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 15, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if not pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f475ec5b770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f475a2a28b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if not pem_lines[-1] != expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 16, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if not not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fdb39ec7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fdb355be8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not not any(line.strip() for line in pem_lines[1:-1]):
> raise ValueError("Private key PEM body is empty")
E ValueError: Private key PEM body is empty
stampbot/github_client.py:262: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM body is empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 17, definition_name: _installation_auth
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -303,7 +303,7 @@
"""
with self._installation_auths_lock:
installation_auth = self._installation_auths.get(installation_id)
- if installation_auth is None:
+ if not installation_auth is None:
# The App credentials must be loaded before an installation auth
# can be derived from them.
self._ensure_initialized()........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7fd1633d79d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140537237570960'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd15bf0ff70>
installation_id = 123456
def _get_installation_client(self, installation_id: int) -> Github:
"""Get an authenticated GitHub client for an installation.
Every call builds a private lazy client on credentials shared per
installation, so the token is reused while connection state stays
private to the calling thread. Binding the client gives the credentials
the requester PyGithub needs for an exchange. The token is then read
here, before any repository request, so the first exchange or a due
refresh happens now and an authentication failure never looks like a
missing repository. A failed exchange leaves the credentials in place;
the next read retries.
Args:
installation_id: GitHub App installation ID.
Returns:
Authenticated Github client instance with timeout and retry configured.
Raises:
github.GithubException: If token exchange fails.
"""
installation_auth = self._installation_auth(installation_id)
client = self._new_client(installation_auth)
> installation_auth.token # noqa: B018
^^^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'token'
stampbot/github_client.py:354: AttributeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - AttributeError: 'NoneType' object has no attribute 'token'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 18, definition_name: _installation_auth
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -307,7 +307,7 @@
# The App credentials must be loaded before an installation auth
# can be derived from them.
self._ensure_initialized()
- assert self._auth is not None # guaranteed by _ensure_initialized
+ assert not self._auth is not None # guaranteed by _ensure_initialized
installation_auth = InstrumentedInstallationAuth(self._auth, installation_id)
self._installation_auths[installation_id] = installation_auth
self._prune_rate_limit_metrics()........................................................................ [ 15%]
......................................F
=================================== FAILURES ===================================
________ TestGetInstallationClient.test_get_installation_client_success ________
self = <tests.test_github_client.TestGetInstallationClient object at 0x7f65b372b9d0>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140074725836176'>
def test_get_installation_client_success(self, installation_auth_cls):
"""Test successful installation client creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client._get_installation_client(123456)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:325:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:352: in _get_installation_client
installation_auth = self._installation_auth(installation_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f65b05bff70>
installation_id = 123456
def _installation_auth(self, installation_id: int) -> InstrumentedInstallationAuth:
"""Return the shared credentials for an installation, creating them on first use.
No network request happens here, so the cache lock is held only briefly.
The token exchange happens when a caller reads ``token``, serialized per
installation by the credentials themselves.
Args:
installation_id: GitHub App installation ID.
Returns:
Credentials whose token PyGithub exchanges on first read and
refreshes before GitHub expires it.
Raises:
RuntimeError: If the GitHub App credentials are not configured.
"""
with self._installation_auths_lock:
installation_auth = self._installation_auths.get(installation_id)
if installation_auth is None:
# The App credentials must be loaded before an installation auth
# can be derived from them.
self._ensure_initialized()
> assert not self._auth is not None # guaranteed by _ensure_initialized
^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError
stampbot/github_client.py:310: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_success - AssertionError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 110 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 19, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if not remaining < 0 or limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7fa30afd7c50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
> assert client._rate_limit_installations == {"9001", "9002"}
E AssertionError: assert set() == {'9001', '9002'}
E
E Extra items in the right set:
E '9001'
E '9002'
E
E Full diff:
E + set()
E - {
E - '9001',
E - '9002',
E - }
tests/test_github_client.py:519: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert set() == {'9001', '9002'}
Extra items in the right set:
'9001'
'9002'
Full diff:
+ set()
- {
- '9001',
- '9002',
- }
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 20, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if not self._bot_login_value is not None and self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7ff7749a4b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:06:17.802898Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: 'NoneType' object is not subscriptable[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object is not subscriptable"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object is not subscriptable"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: 'NoneType' object is not subscriptable", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:06:17.802898Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 21, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if not e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7f37782887d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f3774c0e7b0>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:40:14.879677Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:40:14.879677Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f3774c0e7b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 22, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -681,7 +681,7 @@
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
- if isinstance(content, list):
+ if not isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration........................................................................ [ 15%]
..........................................................F
=================================== FAILURES ===================================
__________________ TestGetRepoFile.test_get_repo_file_success __________________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f07ac54ca50>
def test_get_repo_file_success(self):
"""Test successful file retrieval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_content = Mock()
mock_content.decoded_content = b"file content"
mock_repo = Mock()
mock_repo.get_contents.return_value = mock_content
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_repo_file(
123456,
"owner/repo",
"stampbot.toml",
ref="main",
)
> assert result == "file content"
E AssertionError: assert None == 'file content'
tests/test_github_client.py:985: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_success - AssertionError: assert None == 'file content'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 130 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 23, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if not e.status == 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7fbbb74e3100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140444084073680'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:30:39.027337Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:30:39.027337Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 24, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -712,7 +712,7 @@
)
repository_is_optionally_missing = not_found == "repository"
file_is_confirmed_missing = not_found == "file"
- if repository_is_optionally_missing or file_is_confirmed_missing:
+ if not repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})........................................................................ [ 15%]
.............................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_propagates_unconfirmed_not_found[root_result0] _
self = <tests.test_github_client.TestGetRepoFile object at 0x7fb35b05f230>
root_result = GithubException(404 {"message": "Not Found"})
@pytest.mark.parametrize(
"root_result",
[
GithubException(404, {"message": "Not Found"}, None),
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub root read timed out"),
Mock(),
],
)
def test_get_repo_file_propagates_unconfirmed_not_found(self, root_result):
"""Test a contents 404 fails closed unless the same ref remains readable."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo.get_contents.side_effect = [first, root_result]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(GithubException) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE GithubException
tests/test_github_client.py:1110: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_unconfirmed_not_found[root_result0] - Failed: DID NOT RAISE GithubException
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 133 passed, 3 deselected, 1 warning in 2.92s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 25, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -718,7 +718,7 @@
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
- if repository_is_optionally_missing:
+ if not repository_is_optionally_missing:
logger.debug(
"Optional policy repository %s was not found",
repo_full_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 26, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -812,7 +812,7 @@
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
- if review.user.login == bot_user and review.state in (
+ if not review.user.login == bot_user and review.state in (
"APPROVED",
"DISMISSED",
):........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f18a47d8b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert [{'id': 456, ...3760201584'>}] == [{'id': 123, ...d': 'oldsha'}]
E
E At index 0 diff: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='139743760201584'>} != {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
E Right contains one more item: {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}
E
E Full diff:
E [
E {
E - 'id': 123,
E ? ^^^
E + 'id': 456,
E ? ^^^
E 'state': 'APPROVED',
E + 'commit_id': <Mock name='mock.commit_id' id='139743760201584'>,
E - 'commit_id': 'headsha',
E - },
E - {
E - 'id': 124,
E - 'state': 'DISMISSED',
E - 'commit_id': 'oldsha',
E },
E ]
tests/test_github_client.py:1481: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert [{'id': 456, ...3760201584'>}] == [{'id': 123, ...d': 'oldsha'}]
At index 0 diff: {'id': 456, 'state': 'APPROVED', 'commit_id': <Mock name='mock.commit_id' id='139743760201584'>} != {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
Right contains one more item: {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}
Full diff:
[
{
- 'id': 123,
? ^^^
+ 'id': 456,
? ^^^
'state': 'APPROVED',
+ 'commit_id': <Mock name='mock.commit_id' id='139743760201584'>,
- 'commit_id': 'headsha',
- },
- {
- 'id': 124,
- 'state': 'DISMISSED',
- 'commit_id': 'oldsha',
},
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 27, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1067,7 +1067,7 @@
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
- if e.status == 404:
+ if not e.status == 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f43448c9450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "missing")
> assert result is False
E assert None is False
tests/test_github_client.py:1977: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - assert None is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 28, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1231,7 +1231,7 @@
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
- team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
+ team_slug = team_ref.split("/")[-1] if not "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)........................................................................ [ 15%]
........................................................................ [ 31%]
.........F
=================================== FAILURES ===================================
________ TestGetUserTeamSlugs.test_get_user_team_slugs_with_org_prefix _________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7fbae58411d0>
def test_get_user_team_slugs_with_org_prefix(self):
"""Test team membership check with org/team format."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_user = Mock()
mock_team = Mock()
mock_team.has_in_members.return_value = True
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(123456, "acme", "alice", ["acme/release-team"])
# Should extract "release-team" from "acme/release-team"
> mock_org.get_team_by_slug.assert_called_with("release-team")
tests/test_github_client.py:1775:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization().get_team_by_slug' id='140440635590032'>
args = ('release-team',), kwargs = {}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: get_team_by_slug('release-team')
E Actual: get_team_by_slug('acme/release-team')
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_with_org_prefix - AssertionError: expected call not found.
Expected: get_team_by_slug('release-team')
Actual: get_team_by_slug('acme/release-team')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 153 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 29, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1236,7 +1236,7 @@
try:
team = org.get_team_by_slug(team_slug)
# Check if user is a member
- if team.has_in_members(user): # type: ignore[arg-type]
+ if not team.has_in_members(user): # type: ignore[arg-type]
member_teams.append(team_slug)
logger.debug(
"User %s is a member of team %s",........................................................................ [ 15%]
........................................................................ [ 31%]
........F
=================================== FAILURES ===================================
____________ TestGetUserTeamSlugs.test_get_user_team_slugs_success _____________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7fde180bd090>
def test_get_user_team_slugs_success(self):
"""Test successful team membership check."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Mock user
mock_user = Mock()
mock_user.login = "alice"
# Mock team that has alice as member
mock_team = Mock()
mock_team.has_in_members.return_value = True
# Mock org
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(
123456, "acme", "alice", ["release-team", "deploy-team"]
)
> assert result == ["release-team", "deploy-team"]
E AssertionError: assert [] == ['release-tea...'deploy-team']
E
E Right contains 2 more items, first extra item: 'release-team'
E
E Full diff:
E + []
E - [
E - 'release-team',
E - 'deploy-team',
E - ]
tests/test_github_client.py:1731: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_success - AssertionError: assert [] == ['release-tea...'deploy-team']
Right contains 2 more items, first extra item: 'release-team'
Full diff:
+ []
- [
- 'release-team',
- 'deploy-team',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 152 passed, 3 deselected, 1 warning in 2.87s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 30, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1253,7 +1253,7 @@
logger.debug(
"Could not check team %s membership: %s",
team_slug,
- e.data.get("message", str(e)) if hasattr(e, "data") else str(e),
+ e.data.get("message", str(e)) if not hasattr(e, "data") else str(e),
extra={
"org": org_name,
"team": team_slug,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 0, definition_name: _ensure_initialized
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -192,7 +192,7 @@
timeout=GITHUB_API_TIMEOUT,
retry=_build_retry(),
)
- self._initialized = True
+ self._initialized = False
@property
def integration(self) -> GithubIntegration:........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f77703df770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
client._ensure_initialized()
> assert client._initialized is True
E assert False is True
E + where False = <stampbot.github_client.GitHubAppClient object at 0x7f776baa68b0>._initialized
tests/test_github_client.py:116: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - assert False is True
+ where False = <stampbot.github_client.GitHubAppClient object at 0x7f776baa68b0>._initialized
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 1, definition_name: _new_client
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -281,7 +281,7 @@
auth=installation_auth,
timeout=GITHUB_API_TIMEOUT,
retry=_build_retry(),
- lazy=True,
+ lazy=False,
)
def _installation_auth(self, installation_id: int) -> InstrumentedInstallationAuth:........................................................................ [ 15%]
........................................F
=================================== FAILURES ===================================
_ TestGetInstallationClient.test_get_installation_client_reuses_credentials_per_installation _
self = <tests.test_github_client.TestGetInstallationClient object at 0x7f8df58fab10>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140247633754304'>
def test_get_installation_client_reuses_credentials_per_installation(
self, installation_auth_cls
):
"""Test credentials are shared per installation while each call gets its own client."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_github_cls.side_effect = lambda **kwargs: Mock(name="github", init_kwargs=kwargs)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
first = client._get_installation_client(111)
second = client._get_installation_client(111)
other = client._get_installation_client(222)
# A PyGithub client keeps in-flight request state on its connection,
# so every call gets a private client bound to shared credentials.
assert first is not second
assert other is not first
# Three calls build one client each; credentials are created once per installation.
assert mock_github_cls.call_count == 3
assert installation_auth_cls.call_args_list == [
call(mock_auth_cls.return_value, 111),
call(mock_auth_cls.return_value, 222),
]
kwargs = mock_github_cls.call_args_list[0].kwargs
> assert kwargs["lazy"] is True
E assert False is True
tests/test_github_client.py:397: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_reuses_credentials_per_installation - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 112 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 2, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -456,7 +456,7 @@
"installation_id": installation_id,
},
)
- return True
+ return False
except Exception as e:
duration = time.time() - start_time........................................................................ [ 15%]
.................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_success _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7f3df1e80190>
def test_approve_pr_success(self):
"""Test successful PR approval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is True
E assert False is True
tests/test_github_client.py:631: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:25:43.605068Z[0m [[32m[1minfo [0m] [1mApproved PR #42 in owner/repo [0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35minfo[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
INFO stampbot.github_client:github_client.py:451 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456}, 'event': 'Approved PR #42 in owner/repo', 'logger': 'stampbot.github_client', 'level': 'info', 'timestamp': '2026-09-04T21:25:43.605068Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 121 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 3, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -617,7 +617,7 @@
"review_id": review_id,
},
)
- return True
+ return False
except Exception as e:
duration = time.time() - start_time........................................................................ [ 15%]
.....................................................F
=================================== FAILURES ===================================
______________ TestDismissApproval.test_dismiss_approval_success _______________
self = <tests.test_github_client.TestDismissApproval object at 0x7f3a7344c690>
def test_dismiss_approval_success(self):
"""Test successful approval dismissal."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_review = Mock()
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.dismiss_approval(123456, "owner/repo", 42, 999)
> assert result is True
E assert False is True
tests/test_github_client.py:791: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:03:07.890087Z[0m [[32m[1minfo [0m] [1mDismissed approval for PR #42 in owner/repo[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35minfo[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'review_id': 999}[0m
------------------------------ Captured log call -------------------------------
INFO stampbot.github_client:github_client.py:611 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'review_id': 999}, 'event': 'Dismissed approval for PR #42 in owner/repo', 'logger': 'stampbot.github_client', 'level': 'info', 'timestamp': '2026-09-04T21:03:07.890087Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 125 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 4, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -916,7 +916,7 @@
"installation_id": installation_id,
},
)
- return True
+ return False
except Exception as e:
duration = time.time() - start_time........................................................................ [ 15%]
........................................................................ [ 31%]
.................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_success ________
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7fba13241590>
def test_create_pr_review_comment_success(self):
"""Test successful review comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2098: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:50:57.874710Z[0m [[32m[1minfo [0m] [1mPosted config error review on PR #42 in owner/repo[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35minfo[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
INFO stampbot.github_client:github_client.py:909 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456}, 'event': 'Posted config error review on PR #42 in owner/repo', 'logger': 'stampbot.github_client', 'level': 'info', 'timestamp': '2026-09-04T22:50:57.874710Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 161 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 5, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -995,7 +995,7 @@
"installation_id": installation_id,
},
)
- return True
+ return False
except Exception as e:
duration = time.time() - start_time........................................................................ [ 15%]
........................................................................ [ 31%]
...................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_success ___________
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f68b6cc9810>
def test_create_issue_comment_success(self):
"""Test successful issue comment creation."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_issue = Mock()
mock_repo = Mock()
mock_repo.get_issue.return_value = mock_issue
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
> assert result is True
E assert False is True
tests/test_github_client.py:2187: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:27:05.327771Z[0m [[32m[1minfo [0m] [1mPosted issue comment on #42 in owner/repo[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35minfo[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
INFO stampbot.github_client:github_client.py:988 {'extra': {'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456}, 'event': 'Posted issue comment on #42 in owner/repo', 'logger': 'stampbot.github_client', 'level': 'info', 'timestamp': '2026-09-04T22:27:05.327771Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 163 passed, 3 deselected, 1 warning in 2.90s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 6, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1058,7 +1058,7 @@
self._update_rate_limit_metrics(client, installation_id)
- add_span_attributes(span, {"github.label_found": True})
+ add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return True
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 7, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1060,7 +1060,7 @@
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
- return True
+ return False
except GithubException as e:
duration = time.time() - start_time........................................................................ [ 15%]
........................................................................ [ 31%]
.............F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_true ___________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fbe59131310>
def test_repo_has_label_true(self):
"""Test repo_has_label returns True when label exists."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.return_value = Mock()
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=1, limit=2))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "autoapprove")
> assert result is True
E assert False is True
tests/test_github_client.py:1937: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_true - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 157 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 0, definition_name: _can_read_repo_root
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -88,7 +88,7 @@
try:
root = _get_repo_contents(repo, "", ref)
except Exception:
- return False
+ return True
return isinstance(root, list)
........................................................................ [ 15%]
.............................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_propagates_unconfirmed_not_found[root_result0] _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f66a3eeb230>
root_result = GithubException(404 {"message": "Not Found"})
@pytest.mark.parametrize(
"root_result",
[
GithubException(404, {"message": "Not Found"}, None),
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub root read timed out"),
Mock(),
],
)
def test_get_repo_file_propagates_unconfirmed_not_found(self, root_result):
"""Test a contents 404 fails closed unless the same ref remains readable."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo.get_contents.side_effect = [first, root_result]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(GithubException) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE GithubException
tests/test_github_client.py:1110: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_unconfirmed_not_found[root_result0] - Failed: DID NOT RAISE GithubException
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 133 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 1, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -104,7 +104,7 @@
except GithubException as e:
return e.status == 404
except Exception:
- return False
+ return True
return False
........................................................................ [ 15%]
........................................................................ [ 31%]
.............................F
=================================== FAILURES ===================================
_ TestRepositoryIsMissing.test_repository_is_missing_is_false_unless_github_says_404[outcome2] _
self = <tests.test_github_client.TestRepositoryIsMissing object at 0x7ff4f2783e10>
outcome = TimeoutError('GitHub read timed out')
@pytest.mark.parametrize(
"outcome",
[
None,
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_repository_is_missing_is_false_unless_github_says_404(self, outcome):
"""Test success and every other failure keep the fail-closed path."""
from stampbot.github_client import _repository_is_missing
repo = Mock()
if outcome is not None:
repo.complete.side_effect = outcome
> assert _repository_is_missing(repo) is False
E AssertionError: assert True is False
E + where True = <function _repository_is_missing at 0x7ff4efada560>(<Mock id='140689957460288'>)
tests/test_github_client.py:2317: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepositoryIsMissing::test_repository_is_missing_is_false_unless_github_says_404[outcome2] - AssertionError: assert True is False
+ where True = <function _repository_is_missing at 0x7ff4efada560>(<Mock id='140689957460288'>)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 173 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 2, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -105,7 +105,7 @@
return e.status == 404
except Exception:
return False
- return False
+ return True
NotFoundKind = Literal["repository", "file", "unconfirmed"]........................................................................ [ 15%]
......................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_optional_repository_does_not_hide_unconfirmed_file_not_found _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f473bdd5710>
def test_optional_repository_does_not_hide_unconfirmed_file_not_found(self):
"""Test optional scope does not weaken policy-file read failures."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
first,
GithubException(404, {"message": "Not Found"}, None),
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(GithubException) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE GithubException
tests/test_github_client.py:1359: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_optional_repository_does_not_hide_unconfirmed_file_not_found - Failed: DID NOT RAISE GithubException
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 142 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 3, definition_name: __init__
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -143,7 +143,7 @@
"""Initialize GitHub App client (lazy initialization)."""
self._auth: Auth.AppAuth | None = None
self._integration: GithubIntegration | None = None
- self._initialized = False
+ self._initialized = True
# First use from several webhook threads must build the App credentials once.
self._init_lock = threading.Lock()
# Installation credentials are shared across webhook threads. PyGithub........................................................................ [ 15%]
..................F
=================================== FAILURES ===================================
________ TestGitHubAppClientInit.test_init_creates_uninitialized_client ________
self = <tests.test_github_client.TestGitHubAppClientInit object at 0x7fcbae6c6fd0>
def test_init_creates_uninitialized_client(self):
"""Test that __init__ creates an uninitialized client."""
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
assert client._auth is None
assert client._integration is None
> assert client._initialized is False
E assert True is False
E + where True = <stampbot.github_client.GitHubAppClient object at 0x7fcbab4a3d90>._initialized
tests/test_github_client.py:39: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGitHubAppClientInit::test_init_creates_uninitialized_client - assert True is False
+ where True = <stampbot.github_client.GitHubAppClient object at 0x7fcbab4a3d90>._initialized
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 90 passed, 3 deselected, 1 warning in 2.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 4, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -474,7 +474,7 @@
"error": _sanitize_error(e),
},
)
- return False
+ return True
def get_pr_head_sha(
self,........................................................................ [ 15%]
..................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_failure _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7fb30bb342d0>
def test_approve_pr_failure(self):
"""Test PR approval failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.create_review.side_effect = Exception("API Error")
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is False
E assert True is False
tests/test_github_client.py:670: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:00:04.142624Z[0m [[31m[1merror [0m] [1mFailed to approve PR #42 in owner/repo: API Error[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': 'API Error'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:468 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': 'API Error'}, 'event': 'Failed to approve PR #42 in owner/repo: API Error', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T21:00:04.142624Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_failure - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 122 passed, 3 deselected, 1 warning in 2.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 5, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -638,7 +638,7 @@
"error": _sanitize_error(e),
},
)
- return False
+ return True
def get_repo_file(
self,........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <tests.test_github_client.TestDismissApproval object at 0x7fd2ed8a6ea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
E AssertionError: assert True is False
E + where True = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fd2ea6f27b0>.dismiss_approval
tests/test_github_client.py:864: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:59:19.573167Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Validation Failed"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Validation Failed"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Validation Failed"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Validation Failed"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:59:19.573167Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - AssertionError: assert True is False
+ where True = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7fd2ea6f27b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 2.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 6, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -647,7 +647,7 @@
file_path: str,
ref: str | None = None,
*,
- missing_repository_is_optional: bool = False,
+ missing_repository_is_optional: bool = True,
) -> str | None:
"""Get file content from repository.
........................................................................ [ 15%]
....................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_target_repository_404_never_checks_repository _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f9b82239130>
def test_get_repo_file_target_repository_404_never_checks_repository(self):
"""Test the target repository never spends a request to become optional."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration_cls.return_value = Mock()
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
first,
GithubException(404, {"message": "Not Found"}, None),
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(GithubException) as error:
client.get_repo_file(123456, "owner/repo", "stampbot.toml")
assert error.value is first
> mock_repo.complete.assert_not_called()
tests/test_github_client.py:1284:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().complete' id='140305822831280'>
def assert_not_called(self):
"""assert that the mock was never called.
"""
if self.call_count != 0:
msg = ("Expected '%s' to not have been called. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'complete' to not have been called. Called 1 times.
E Calls: [call()].
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:947: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:54:23.118894Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:54:23.118894Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_target_repository_404_never_checks_repository - AssertionError: Expected 'complete' to not have been called. Called 1 times.
Calls: [call()].
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 140 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 7, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -937,7 +937,7 @@
"error": _sanitize_error(e),
},
)
- return False
+ return True
def create_issue_comment(
self,........................................................................ [ 15%]
........................................................................ [ 31%]
..................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_failure ________
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7f544e4ad6d0>
def test_create_pr_review_comment_failure(self):
"""Test review comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_pull.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
> assert result is False
E assert True is False
tests/test_github_client.py:2140: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:27:42.214501Z[0m [[33m[1mwarning [0m] [1mFailed to post config error review for PR #42 in owner/repo: API Error[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': 'API Error'}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:928 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': 'API Error'}, 'event': 'Failed to post config error review for PR #42 in owner/repo: API Error', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:27:42.214501Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_failure - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 162 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 8, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1018,7 +1018,7 @@
"error": _sanitize_error(e),
},
)
- return False
+ return True
def repo_has_label(
self,........................................................................ [ 15%]
........................................................................ [ 31%]
....................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_failure ___________
self = <tests.test_github_client.TestCreateIssueComment object at 0x7fb36066d950>
def test_create_issue_comment_failure(self):
"""Test issue comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_issue.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
> assert result is False
E assert True is False
tests/test_github_client.py:2230: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:30:20.077496Z[0m [[33m[1mwarning [0m] [1mFailed to post issue comment on #42 in owner/repo: API Error[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': 'API Error'}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1009 {'extra': {'repo': 'owner/repo', 'issue_number': 42, 'installation_id': 123456, 'error': 'API Error'}, 'event': 'Failed to post issue comment on #42 in owner/repo: API Error', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:30:20.077496Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_failure - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 164 passed, 3 deselected, 1 warning in 2.96s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 9, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1068,7 +1068,7 @@
github_api_requests_total.labels(operation="get_label", status="failure").inc()
if e.status == 404:
- add_span_attributes(span, {"github.label_found": False})
+ add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return False
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 10, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1070,7 +1070,7 @@
if e.status == 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
- return False
+ return True
set_span_error(span, e)
logger.debug(........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f15d3e81450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "missing")
> assert result is False
E assert True is False
tests/test_github_client.py:1977: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 11, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1191,7 +1191,7 @@
"error": _sanitize_error(e),
},
)
- return False
+ return True
def get_user_team_slugs(
self,........................................................................ [ 15%]
........................................................................ [ 31%]
......F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_error _____________
self = <tests.test_github_client.TestUserHasPermission object at 0x7f8858b3f360>
def test_user_has_permission_error(self):
"""Test permission check returns False on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.user_has_permission(123456, "owner/repo", "carol", "maintain")
> assert result is False
E assert True is False
tests/test_github_client.py:1639: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:19:01.887946Z[0m [[33m[1mwarning [0m] [1mFailed to check collaborator permission for carol in owner/repo: API Error[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'username': 'carol', 'installation_id': 123456, 'error': 'API Error'}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:1182 {'extra': {'repo': 'owner/repo', 'username': 'carol', 'installation_id': 123456, 'error': 'API Error'}, 'event': 'Failed to check collaborator permission for carol in owner/repo: API Error', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:19:01.887946Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_error - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 150 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 0, definition_name: _classify_not_found
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -119,7 +119,7 @@
``.github`` repository. A readable repository root proves the file is
absent. Anything else stays unconfirmed and the caller fails closed.
"""
- if optional_repo and _repository_is_missing(repo):
+ if optional_repo or _repository_is_missing(repo):
return "repository"
if _can_read_repo_root(repo, ref):
return "file"........................................................................ [ 15%]
...................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_accepts_missing_optional_repository_with_lazy_client _
self = <tests.test_github_client.TestGetRepoFile object at 0x7fcc57209400>
def test_get_repo_file_accepts_missing_optional_repository_with_lazy_client(self):
"""Test a lazy client confirms the optional repository itself is missing."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_integration_cls.return_value = mock_integration
# Lazy get_repo never raises. The file read and the root read both
# 404, and only the repository request proves the repo is absent.
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
GithubException(404, {"message": "Not Found"}, None),
]
mock_repo.complete.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_repo_file(
123456,
"owner/.github",
"stampbot.toml",
missing_repository_is_optional=True,
)
assert result is None
> mock_repo.complete.assert_called_once_with()
tests/test_github_client.py:1244:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().complete' id='140515486553424'>
args = (), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'complete' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_accepts_missing_optional_repository_with_lazy_client - AssertionError: Expected 'complete' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 139 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 1, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None or self._bot_login_value[1] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7f34bcb34b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
tests/test_github_client.py:1481: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:31:39.221242Z[0m [[31m[1merror [0m] [1mFailed to find bot approval reviews for PR #42 in owner/repo: 'NoneType' object is not subscriptable[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object is not subscriptable"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:851 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object is not subscriptable"}, 'event': "Failed to find bot approval reviews for PR #42 in owner/repo: 'NoneType' object is not subscriptable", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:31:39.221242Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert None == [{'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}, {'id': 124, 'state': 'DISMISSED', 'commit_id': 'oldsha'}]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.93s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceAndWithOr, occurrence: 2, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if e.status == 404 or repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 3, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -812,7 +812,7 @@
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
- if review.user.login == bot_user and review.state in (
+ if review.user.login == bot_user or review.state in (
"APPROVED",
"DISMISSED",
):........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fb1ce228b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert [{'id': 123, ...1587075264'>}] == [{'id': 123, ...d': 'oldsha'}]
E
E Left contains 2 more items, first extra item: {'id': 125, 'state': 'COMMENTED', 'commit_id': <Mock name='mock.commit_id' id='140401587074928'>}
E
E Full diff:
E [
E {
E 'id': 123,
E 'state': 'APPROVED',
E 'commit_id': 'headsha',
E },
E {
E 'id': 124,
E 'state': 'DISMISSED',
E 'commit_id': 'oldsha',
E },
E + {
E + 'id': 125,
E + 'state': 'COMMENTED',
E + 'commit_id': <Mock name='mock.commit_id' id='140401587074928'>,
E + },
E + {
E + 'id': 456,
E + 'state': 'APPROVED',
E + 'commit_id': <Mock name='mock.commit_id' id='140401587075264'>,
E + },
E ]
tests/test_github_client.py:1481: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert [{'id': 123, ...1587075264'>}] == [{'id': 123, ...d': 'oldsha'}]
Left contains 2 more items, first extra item: {'id': 125, 'state': 'COMMENTED', 'commit_id': <Mock name='mock.commit_id' id='140401587074928'>}
Full diff:
[
{
'id': 123,
'state': 'APPROVED',
'commit_id': 'headsha',
},
{
'id': 124,
'state': 'DISMISSED',
'commit_id': 'oldsha',
},
+ {
+ 'id': 125,
+ 'state': 'COMMENTED',
+ 'commit_id': <Mock name='mock.commit_id' id='140401587074928'>,
+ },
+ {
+ 'id': 456,
+ 'state': 'APPROVED',
+ 'commit_id': <Mock name='mock.commit_id' id='140401587075264'>,
+ },
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.93s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 0, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) < 3 and not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 1, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < 0 and limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 2, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 422 and pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <tests.test_github_client.TestDismissApproval object at 0x7f3021a2aea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
E AssertionError: assert True is False
E + where True = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f301e8267b0>.dismiss_approval
tests/test_github_client.py:864: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:40:08.626946Z[0m [[32m[1minfo [0m] [1mReview 789 on PR #42 in owner/repo was already dismissed[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35minfo[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'review_id': 789}[0m
[2m2026-09-04T20:40:08.627183Z[0m [[32m[1minfo [0m] [1mDismissed approval for PR #42 in owner/repo[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35minfo[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'review_id': 789}[0m
------------------------------ Captured log call -------------------------------
INFO stampbot.github_client:github_client.py:590 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'review_id': 789}, 'event': 'Review 789 on PR #42 in owner/repo was already dismissed', 'logger': 'stampbot.github_client', 'level': 'info', 'timestamp': '2026-09-04T20:40:08.626946Z'}
INFO stampbot.github_client:github_client.py:611 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'review_id': 789}, 'event': 'Dismissed approval for PR #42 in owner/repo', 'logger': 'stampbot.github_client', 'level': 'info', 'timestamp': '2026-09-04T20:40:08.627183Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - AssertionError: assert True is False
+ where True = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f301e8267b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 2.88s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 3, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -670,7 +670,7 @@
{
"github.repo": repo_full_name,
"github.file_path": file_path,
- "github.ref": ref or "default",
+ "github.ref": ref and "default",
"github.installation_id": installation_id,
},
) as span:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 4, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -712,7 +712,7 @@
)
repository_is_optionally_missing = not_found == "repository"
file_is_confirmed_missing = not_found == "file"
- if repository_is_optionally_missing or file_is_confirmed_missing:
+ if repository_is_optionally_missing and file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7fdaf0577100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140578252764368'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:44:50.980906Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T22:44:50.980906Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.92s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceContinueWithBreak, occurrence: 0, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1260,7 +1260,7 @@
"username": username,
},
)
- continue
+ break
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 0, definition_name: _can_read_repo_root
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -87,7 +87,7 @@
"""Confirm that a file 404 did not conceal a repository, auth, or ref failure."""
try:
root = _get_repo_contents(repo, "", ref)
- except Exception:
+ except CosmicRayTestingException:
return False
return isinstance(root, list)
........................................................................ [ 15%]
.............................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_propagates_unconfirmed_not_found[root_result0] _
self = <stampbot.github_client.GitHubAppClient object at 0x7f51f19d71b0>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139989917632896'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
repo = <Mock name='Github().get_repo()' id='139989917632560'>, ref = None
def _can_read_repo_root(repo: Repository, ref: str | None) -> bool:
"""Confirm that a file 404 did not conceal a repository, auth, or ref failure."""
try:
> root = _get_repo_contents(repo, "", ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:89:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139989917632896'>
args = ('',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f51f4b17230>
root_result = GithubException(404 {"message": "Not Found"})
@pytest.mark.parametrize(
"root_result",
[
GithubException(404, {"message": "Not Found"}, None),
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub root read timed out"),
Mock(),
],
)
def test_get_repo_file_propagates_unconfirmed_not_found(self, root_result):
"""Test a contents 404 fails closed unless the same ref remains readable."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
first = GithubException(404, {"message": "Not Found"}, None)
mock_repo.get_contents.side_effect = [first, root_result]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(GithubException) as error:
> client.get_repo_file(123456, "owner/repo", "stampbot.toml")
tests/test_github_client.py:1111:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:710: in get_repo_file
not_found = _classify_not_found(
stampbot/github_client.py:124: in _classify_not_found
if _can_read_repo_root(repo, ref):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
repo = <Mock name='Github().get_repo()' id='139989917632560'>, ref = None
def _can_read_repo_root(repo: Repository, ref: str | None) -> bool:
"""Confirm that a file 404 did not conceal a repository, auth, or ref failure."""
try:
root = _get_repo_contents(repo, "", ref)
> except CosmicRayTestingException:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:90: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_unconfirmed_not_found[root_result0] - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 133 passed, 3 deselected, 1 warning in 3.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 1, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -101,7 +101,7 @@
"""
try:
repo.complete()
- except GithubException as e:
+ except CosmicRayTestingException as e:
return e.status == 404
except Exception:
return False........................................................................ [ 15%]
...................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_accepts_missing_optional_repository_with_lazy_client _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb8e11bb890>
installation_id = 123456, repo_full_name = 'owner/.github'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = True
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140432022175824'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
repo = <Mock name='Github().get_repo()' id='140432022178512'>
def _repository_is_missing(repo: Repository) -> bool:
"""Confirm that GitHub returns 404 for the repository itself.
Lazy repository objects never request ``GET /repos/{owner}/{repo}`` on their
own, so a contents 404 cannot distinguish a missing file from a missing or
inaccessible repository. This check settles that question with one request.
"""
try:
> repo.complete()
stampbot/github_client.py:103:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().complete' id='140432022176160'>
args = (), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7fb8e43e9400>
def test_get_repo_file_accepts_missing_optional_repository_with_lazy_client(self):
"""Test a lazy client confirms the optional repository itself is missing."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_integration_cls.return_value = mock_integration
# Lazy get_repo never raises. The file read and the root read both
# 404, and only the repository request proves the repo is absent.
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
GithubException(404, {"message": "Not Found"}, None),
]
mock_repo.complete.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/.github",
"stampbot.toml",
missing_repository_is_optional=True,
)
tests/test_github_client.py:1236:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:710: in get_repo_file
not_found = _classify_not_found(
stampbot/github_client.py:122: in _classify_not_found
if optional_repo and _repository_is_missing(repo):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
repo = <Mock name='Github().get_repo()' id='140432022178512'>
def _repository_is_missing(repo: Repository) -> bool:
"""Confirm that GitHub returns 404 for the repository itself.
Lazy repository objects never request ``GET /repos/{owner}/{repo}`` on their
own, so a contents 404 cannot distinguish a missing file from a missing or
inaccessible repository. This check settles that question with one request.
"""
try:
repo.complete()
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:104: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_accepts_missing_optional_repository_with_lazy_client - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 139 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 2, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -103,7 +103,7 @@
repo.complete()
except GithubException as e:
return e.status == 404
- except Exception:
+ except CosmicRayTestingException:
return False
return False
........................................................................ [ 15%]
........................................................................ [ 31%]
.............................F
=================================== FAILURES ===================================
_ TestRepositoryIsMissing.test_repository_is_missing_is_false_unless_github_says_404[outcome2] _
repo = <Mock id='140267009857856'>
def _repository_is_missing(repo: Repository) -> bool:
"""Confirm that GitHub returns 404 for the repository itself.
Lazy repository objects never request ``GET /repos/{owner}/{repo}`` on their
own, so a contents 404 cannot distinguish a missing file from a missing or
inaccessible repository. This check settles that question with one request.
"""
try:
> repo.complete()
stampbot/github_client.py:103:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='mock.complete' id='140267009856848'>, args = (), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E TimeoutError: GitHub read timed out
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: TimeoutError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepositoryIsMissing object at 0x7f9278c9fe10>
outcome = TimeoutError('GitHub read timed out')
@pytest.mark.parametrize(
"outcome",
[
None,
GithubException(403, {"message": "Forbidden"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_repository_is_missing_is_false_unless_github_says_404(self, outcome):
"""Test success and every other failure keep the fail-closed path."""
from stampbot.github_client import _repository_is_missing
repo = Mock()
if outcome is not None:
repo.complete.side_effect = outcome
> assert _repository_is_missing(repo) is False
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:2317:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
repo = <Mock id='140267009857856'>
def _repository_is_missing(repo: Repository) -> bool:
"""Confirm that GitHub returns 404 for the repository itself.
Lazy repository objects never request ``GET /repos/{owner}/{repo}`` on their
own, so a contents 404 cannot distinguish a missing file from a missing or
inaccessible repository. This check settles that question with one request.
"""
try:
repo.complete()
except GithubException as e:
return e.status == 404
> except CosmicRayTestingException:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:106: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepositoryIsMissing::test_repository_is_missing_is_false_unless_github_says_404[outcome2] - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 173 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 3, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -239,7 +239,7 @@
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
- except FileNotFoundError:
+ except CosmicRayTestingException:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)........................................................................ [ 15%]
............................F
=================================== FAILURES ===================================
________ TestLoadPrivateKey.test_load_private_key_raises_on_file_error _________
self = <stampbot.github_client.GitHubAppClient object at 0x7fb9886b1a90>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
> with Path(key).expanduser().open(encoding="utf-8") as key_file:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:235:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = PosixPath('/nonexistent/path/to/key.pem'), mode = 'r', buffering = -1
encoding = 'utf-8', errors = None, newline = None
def open(self, mode='r', buffering=-1, encoding=None,
errors=None, newline=None):
"""
Open the file pointed to by this path and return a file object, as
the built-in open() function does.
"""
if "b" not in mode:
encoding = io.text_encoding(encoding)
> return io.open(self, mode, buffering, encoding, errors, newline)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E FileNotFoundError: [Errno 2] No such file or directory: '/nonexistent/path/to/key.pem'
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/pathlib/__init__.py:771: FileNotFoundError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7fb98b6fe9e0>
def test_load_private_key_raises_on_file_error(self):
"""Test that _load_private_key raises when file doesn't exist."""
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = "/nonexistent/path/to/key.pem"
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(ValueError, match="Invalid private key path"):
> client._load_private_key()
tests/test_github_client.py:184:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb9886b1a90>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
> except CosmicRayTestingException:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:242: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_raises_on_file_error - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 100 passed, 3 deselected, 1 warning in 2.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 4, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -241,7 +241,7 @@
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
- except OSError as e:
+ except CosmicRayTestingException as e:
logger.error("Failed to read private key file: %s", e)
raise
........................................................................ [ 15%]
.............................F
=================================== FAILURES ===================================
________ TestLoadPrivateKey.test_load_private_key_raises_on_read_error _________
self = <stampbot.github_client.GitHubAppClient object at 0x7fa60904fa10>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
> with Path(key).expanduser().open(encoding="utf-8") as key_file:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:235:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='open' id='140351091572816'>, args = ()
kwargs = {'encoding': 'utf-8'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E PermissionError: Access denied
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: PermissionError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7fa60d9e3890>
tmp_path = PosixPath('/tmp/pytest-of-runner/pytest-589/test_load_private_key_raises_o0')
def test_load_private_key_raises_on_read_error(self, tmp_path):
"""Test that _load_private_key raises when file read fails."""
key_file = tmp_path / "private-key.pem"
key_file.write_text("dummy")
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = str(key_file)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with patch("pathlib.Path.open", side_effect=PermissionError("Access denied")):
with pytest.raises(PermissionError, match="Access denied"):
> client._load_private_key()
tests/test_github_client.py:200:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa60904fa10>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:244: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_raises_on_read_error - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 101 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 5, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -378,7 +378,7 @@
github_api_rate_limit_limit.labels(installation_id=label).set(limit)
with self._installation_auths_lock:
self._rate_limit_installations.add(label)
- except Exception:
+ except CosmicRayTestingException:
# Don't fail operations due to rate limit metric errors
pass
........................................................................ [ 15%]
................................................F
=================================== FAILURES ===================================
_ TestUpdateRateLimitMetrics.test_update_rate_limit_metrics_handles_exception __
self = <stampbot.github_client.GitHubAppClient object at 0x7fec3c9a1bd0>
client = <Mock id='140652605615680'>, installation_id = 123456
def _update_rate_limit_metrics(self, client: Github, installation_id: int) -> None:
"""Update rate limit metrics from the client's last GitHub response.
GitHub returns the installation's remaining quota in the
``x-ratelimit-*`` headers of every response, so this reads PyGithub's
record of those headers instead of spending a request on
``GET /rate_limit``. The requester is read directly because
``Github.rate_limiting`` falls back to that request when no response
has carried the headers yet.
Args:
client: Authenticated GitHub client.
installation_id: GitHub App installation ID for metric labels.
"""
try:
> remaining, limit = client.requester.rate_limiting
^^^^^^^^^^^^^^^^
E ValueError: too many values to unpack (expected 2)
stampbot/github_client.py:372: ValueError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestUpdateRateLimitMetrics object at 0x7fec3fca2d70>
def test_update_rate_limit_metrics_handles_exception(self):
"""Test that rate limit metrics errors are silently ignored."""
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
mock_github_client = Mock()
mock_github_client.requester.rate_limiting = "not a tuple"
# Should not raise - errors are silently ignored
> client._update_rate_limit_metrics(mock_github_client, 123456)
tests/test_github_client.py:589:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fec3c9a1bd0>
client = <Mock id='140652605615680'>, installation_id = 123456
def _update_rate_limit_metrics(self, client: Github, installation_id: int) -> None:
"""Update rate limit metrics from the client's last GitHub response.
GitHub returns the installation's remaining quota in the
``x-ratelimit-*`` headers of every response, so this reads PyGithub's
record of those headers instead of spending a request on
``GET /rate_limit``. The requester is read directly because
``Github.rate_limiting`` falls back to that request when no response
has carried the headers yet.
Args:
client: Authenticated GitHub client.
installation_id: GitHub App installation ID for metric labels.
"""
try:
remaining, limit = client.requester.rate_limiting
if remaining < 0 or limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)
github_api_rate_limit_remaining.labels(installation_id=label).set(remaining)
github_api_rate_limit_limit.labels(installation_id=label).set(limit)
with self._installation_auths_lock:
self._rate_limit_installations.add(label)
> except CosmicRayTestingException:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:381: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUpdateRateLimitMetrics::test_update_rate_limit_metrics_handles_exception - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 120 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 6, definition_name: approve_pr
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -458,7 +458,7 @@
)
return True
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="failure").inc()........................................................................ [ 15%]
..................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_failure _____________________
self = <stampbot.github_client.GitHubAppClient object at 0x7ff8db3493b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
> pr.create_review(
body=comment,
event="APPROVE",
)
stampbot/github_client.py:437:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().create_review' id='140706800075504'>
args = (), kwargs = {'body': 'Auto-approved by Stampbot', 'event': 'APPROVE'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestApprovePR object at 0x7ff8de4402d0>
def test_approve_pr_failure(self):
"""Test PR approval failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.create_review.side_effect = Exception("API Error")
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.approve_pr(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:668:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7ff8db3493b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
pr.create_review(
body=comment,
event="APPROVE",
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="approve").observe(duration)
github_api_requests_total.labels(operation="approve", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "approved"})
set_span_ok(span)
logger.info(
f"Approved PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:461: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_failure - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 122 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 7, definition_name: get_pr_head_sha
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -519,7 +519,7 @@
return head_sha
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="failure").inc()........................................................................ [ 15%]
....................................................F
=================================== FAILURES ===================================
_________ TestGetPRHeadSHA.test_get_pr_head_sha_returns_none_on_error __________
self = <stampbot.github_client.GitHubAppClient object at 0x7f47f1331ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:507:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='139946966821792'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetPRHeadSHA object at 0x7f47f4420550>
def test_get_pr_head_sha_returns_none_on_error(self):
"""Test PR head SHA lookup returns None on API errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_pr_head_sha(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:745:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f47f1331ef0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def get_pr_head_sha(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> str | None:
"""Get the current head SHA for a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Current pull request head SHA, or None on error.
"""
start_time = time.time()
with create_span(
"github.get_pr_head_sha",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
head_sha: str = pr.head.sha
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_pull").observe(duration)
github_api_requests_total.labels(operation="get_pull", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.head_sha": head_sha})
set_span_ok(span)
return head_sha
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:522: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetPRHeadSHA::test_get_pr_head_sha_returns_none_on_error - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 124 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 8, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -580,7 +580,7 @@
review = pr.get_review(review_id)
try:
review.dismiss(message)
- except GithubException as e:
+ except CosmicRayTestingException as e:
# GitHub answers 422 when a review is already dismissed,
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7f53218987d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f531e6927b0>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:41:52.232747Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: name 'CosmicRayTestingException' is not defined[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "name 'CosmicRayTestingException' is not defined"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "name 'CosmicRayTestingException' is not defined"}, 'event': "Failed to dismiss approval for PR #42 in owner/repo: name 'CosmicRayTestingException' is not defined", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:41:52.232747Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f531e6927b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 9, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -619,7 +619,7 @@
)
return True
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="failure").inc()........................................................................ [ 15%]
.......................................................F
=================================== FAILURES ===================================
_ TestDismissApproval.test_dismiss_approval_422_with_review_still_approved_fails _
self = <stampbot.github_client.GitHubAppClient object at 0x7fda8e1ae7b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
> review.dismiss(message)
stampbot/github_client.py:582:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().get_review().dismiss' id='140576664008224'>
args = ('Duplicate',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 422 {"message": "Validation Failed"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestDismissApproval object at 0x7fda91472ea0>
def test_dismiss_approval_422_with_review_still_approved_fails(self):
"""Test a 422 that leaves the review approved is reported as a failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "APPROVED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Validation Failed"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is False
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:864:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fda8e1ae7b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
review_id = 789, message = 'Duplicate'
def dismiss_approval(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
review_id: int,
message: str = "Approval dismissed by Stampbot",
) -> bool:
"""Dismiss a pull request approval.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
review_id: Review ID to dismiss
message: Dismissal message
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.dismiss_approval",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.review_id": review_id,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Get the review and dismiss it
review = pr.get_review(review_id)
try:
review.dismiss(message)
except GithubException as e:
# GitHub answers 422 when a review is already dismissed,
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",
review_id,
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"review_id": review_id,
},
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="dismiss").observe(duration)
github_api_requests_total.labels(operation="dismiss", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "dismissed"})
set_span_ok(span)
logger.info(
f"Dismissed approval for PR #{pr_number} in {repo_full_name}",
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
"review_id": review_id,
},
)
return True
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:622: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_422_with_review_still_approved_fails - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 127 passed, 3 deselected, 1 warning in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 10, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -702,7 +702,7 @@
return content.decoded_content.decode("utf-8")
- except GithubException as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <stampbot.github_client.GitHubAppClient object at 0x7f8fd9e8a170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140255807026384'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f8fdd083100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f8fd9e8a170>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'nonexistent.toml', ref = None
missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:705: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 11, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -757,7 +757,7 @@
)
raise
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="error").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
.F
=================================== FAILURES ===================================
____ TestGetRepoFile.test_get_repo_file_propagates_read_failures[failure2] _____
self = <stampbot.github_client.GitHubAppClient object at 0x7f75356c4050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
> content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:683:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140141387928240'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E TimeoutError: GitHub read timed out
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: TimeoutError
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetRepoFile object at 0x7f7538d8ee10>
failure = TimeoutError('GitHub read timed out')
@pytest.mark.parametrize(
"failure",
[
GithubException(403, {"message": "Forbidden"}, None),
GithubException(500, {"message": "Error"}, None),
TimeoutError("GitHub read timed out"),
],
)
def test_get_repo_file_propagates_read_failures(self, failure):
"""Test non-404 policy failures reach the handler's fail-closed path."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = failure
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
with pytest.raises(type(failure), match=str(failure)):
> client.get_repo_file(123456, "owner/repo", "stampbot.toml")
tests/test_github_client.py:1410:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f75356c4050>
installation_id = 123456, repo_full_name = 'owner/repo'
file_path = 'stampbot.toml', ref = None, missing_repository_is_optional = False
def get_repo_file(
self,
installation_id: int,
repo_full_name: str,
file_path: str,
ref: str | None = None,
*,
missing_repository_is_optional: bool = False,
) -> str | None:
"""Get file content from repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
file_path: Path to file in repository
ref: Git reference (branch, tag, commit). Defaults to default branch
missing_repository_is_optional: Treat a repository-level 404 as not
found. This is only for optional fallback repositories.
Returns:
File content as string, or None if not found
"""
start_time = time.time()
repo: Repository | None = None
with create_span(
"github.get_file",
{
"github.repo": repo_full_name,
"github.file_path": file_path,
"github.ref": ref or "default",
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
# Lazy: this builds the repository URL without a request, so
# ``repo`` stays None only when authentication itself failed.
repo = client.get_repo(repo_full_name)
content = _get_repo_contents(repo, file_path, ref)
if isinstance(content, list):
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(
duration
)
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
return None
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
github_api_requests_total.labels(operation="get_file", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "found"})
set_span_ok(span)
return content.decoded_content.decode("utf-8")
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
if e.status == 404 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)
repository_is_optionally_missing = not_found == "repository"
file_is_confirmed_missing = not_found == "file"
if repository_is_optionally_missing or file_is_confirmed_missing:
github_api_requests_total.labels(operation="get_file", status="not_found").inc()
add_span_attributes(span, {"github.result": "not_found"})
set_span_ok(span)
if repository_is_optionally_missing:
logger.debug(
"Optional policy repository %s was not found",
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
else:
logger.debug(
"Policy file %s was not found in %s",
file_path,
repo_full_name,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
return None
github_api_requests_total.labels(operation="get_file", status="error").inc()
add_span_attributes(span, {"github.result": "error"})
set_span_error(span, e)
logger.warning(
"GitHub could not read %s from %s (status %s)",
file_path,
repo_full_name,
e.status,
extra={
"repo": repo_full_name,
"file_path": file_path,
"installation_id": installation_id,
},
)
raise
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:760: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_propagates_read_failures[failure2] - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 145 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 12, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -839,7 +839,7 @@
return bot_reviews
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
...F
=================================== FAILURES ===================================
_ TestFindBotApprovalReviews.test_find_bot_approval_reviews_returns_none_on_error _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd06af9fcf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
> repo = client.get_repo(repo_full_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:809:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo' id='140533121020464'>
args = ('owner/repo',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7fd06e134cd0>
def test_find_bot_approval_reviews_returns_none_on_error(self):
"""Test that a listing failure is reported as None, distinct from no reviews."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_repo.side_effect = Exception("API Error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1523:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fd06af9fcf0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
def find_bot_approval_reviews(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
) -> list[BotReview] | None:
"""Find approval review states created by this bot on a PR.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
Returns:
Review state details for bot approval reviews, or None when GitHub
could not be read. Callers decide what an unknown state means for
them; an empty list always means "no Stampbot approvals".
"""
start_time = time.time()
with create_span(
"github.find_bot_approval_reviews",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
bot_user = self._bot_login()
bot_reviews: list[BotReview] = []
for review in pr.get_reviews():
if review.user.login == bot_user and review.state in (
"APPROVED",
"DISMISSED",
):
bot_reviews.append(
{
"id": review.id,
"state": review.state,
"commit_id": getattr(review, "commit_id", None),
}
)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="find_reviews").observe(
duration
)
github_api_requests_total.labels(operation="find_reviews", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span, {"github.reviews_found": len(bot_reviews), "github.bot_user": bot_user}
)
set_span_ok(span)
return bot_reviews
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:842: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_returns_none_on_error - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 147 passed, 3 deselected, 1 warning in 3.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 13, definition_name: create_pr_review_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -918,7 +918,7 @@
)
return True
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="failure").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
..................F
=================================== FAILURES ===================================
_______ TestCreatePrReviewComment.test_create_pr_review_comment_failure ________
self = <stampbot.github_client.GitHubAppClient object at 0x7fb39acc0af0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> pr = repo.get_pull(pr_number)
^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:896:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull' id='140409371570064'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreatePrReviewComment object at 0x7fb39e2896d0>
def test_create_pr_review_comment_failure(self):
"""Test review comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_pull.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_pr_review_comment(
123456,
"owner/repo",
42,
"Config error",
)
tests/test_github_client.py:2133:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fb39acc0af0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
message = 'Config error'
def create_pr_review_comment(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
message: str,
) -> bool:
"""Create a comment review on a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
message: Review comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_review_comment",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
pr.create_review(body=message, event="COMMENT")
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="comment").observe(duration)
github_api_requests_total.labels(operation="comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted config error review on PR #%d in %s",
pr_number,
repo_full_name,
extra={
"repo": repo_full_name,
"pr_number": pr_number,
"installation_id": installation_id,
},
)
return True
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:921: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreatePrReviewComment::test_create_pr_review_comment_failure - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 162 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 14, definition_name: create_issue_comment
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -997,7 +997,7 @@
)
return True
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
....................F
=================================== FAILURES ===================================
___________ TestCreateIssueComment.test_create_issue_comment_failure ___________
self = <stampbot.github_client.GitHubAppClient object at 0x7f693bcfceb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> issue = repo.get_issue(issue_number)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:973:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_issue' id='140089949251840'>
args = (42,), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestCreateIssueComment object at 0x7f694325d950>
def test_create_issue_comment_failure(self):
"""Test issue comment creation handles errors."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_issue.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.create_issue_comment(
123456,
"owner/repo",
42,
"Help text",
)
tests/test_github_client.py:2223:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f693bcfceb0>
installation_id = 123456, repo_full_name = 'owner/repo', issue_number = 42
message = 'Help text'
def create_issue_comment(
self,
installation_id: int,
repo_full_name: str,
issue_number: int,
message: str,
) -> bool:
"""Create a comment on an issue or pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
issue_number: Issue or pull request number
message: Comment body
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.create_issue_comment",
{
"github.repo": repo_full_name,
"github.issue_number": issue_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
issue = repo.get_issue(issue_number)
issue.create_comment(message)
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="issue_comment").observe(
duration
)
github_api_requests_total.labels(operation="issue_comment", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.result": "commented"})
set_span_ok(span)
logger.info(
"Posted issue comment on #%d in %s",
issue_number,
repo_full_name,
extra={
"repo": repo_full_name,
"issue_number": issue_number,
"installation_id": installation_id,
},
)
return True
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:1000: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestCreateIssueComment::test_create_issue_comment_failure - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 164 passed, 3 deselected, 1 warning in 3.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 15, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1062,7 +1062,7 @@
set_span_ok(span)
return True
- except GithubException as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fc52dab8a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140484851045040'>
args = ('missing',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: GithubException
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7fc531081450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "missing")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1975:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fc52dab8a50>
installation_id = 123456, repo_full_name = 'owner/repo', label_name = 'missing'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:1065: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 16, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1087,7 +1087,7 @@
)
return None
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()........................................................................ [ 15%]
........................................................................ [ 31%]
................F
=================================== FAILURES ===================================
__________________ TestRepoHasLabel.test_repo_has_label_error __________________
self = <stampbot.github_client.GitHubAppClient object at 0x7faef60a25d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
> repo.get_label(label_name).complete()
^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1053:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_label' id='140389424250032'>
args = ('autoapprove',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestRepoHasLabel object at 0x7faef9327950>
def test_repo_has_label_error(self):
"""Test repo_has_label returns None on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.repo_has_label(123456, "owner/repo", "autoapprove")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:2049:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7faef60a25d0>
installation_id = 123456, repo_full_name = 'owner/repo'
label_name = 'autoapprove'
def repo_has_label(
self,
installation_id: int,
repo_full_name: str,
label_name: str,
) -> bool | None:
"""Check whether a repository has a label.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
label_name: Label name to look up
Returns:
True if label exists, False if not found, None if unknown due to error
"""
start_time = time.time()
with create_span(
"github.get_label",
{
"github.repo": repo_full_name,
"github.label": label_name,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
# The lazy client defers the request until the label is read.
repo.get_label(label_name).complete()
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(span, {"github.label_found": True})
set_span_ok(span)
return True
except GithubException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
if e.status == 404:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False
set_span_error(span, e)
logger.debug(
"Could not verify label %s in %s: %s",
label_name,
repo_full_name,
_sanitize_error(e),
extra={
"repo": repo_full_name,
"label": label_name,
"installation_id": installation_id,
"error": _sanitize_error(e),
},
)
return None
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:1090: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_error - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 160 passed, 3 deselected, 1 warning in 3.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ExceptionReplacer, occurrence: 17, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1145,7 +1145,7 @@
try:
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
- except ValueError:
+ except CosmicRayTestingException:
permission_index = -1
required_index = 99
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 18, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1170,7 +1170,7 @@
return has_permission
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
......F
=================================== FAILURES ===================================
_____________ TestUserHasPermission.test_user_has_permission_error _____________
self = <stampbot.github_client.GitHubAppClient object at 0x7fcba9278410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
> permission = repo.get_collaborator_permission(username)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1143:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_collaborator_permission' id='140512693301472'>
args = ('carol',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestUserHasPermission object at 0x7fcbac91f360>
def test_user_has_permission_error(self):
"""Test permission check returns False on error."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_collaborator_permission.side_effect = Exception("API Error")
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.user_has_permission(123456, "owner/repo", "carol", "maintain")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1637:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fcba9278410>
installation_id = 123456, repo_full_name = 'owner/repo', username = 'carol'
required_permission = 'maintain'
def user_has_permission(
self,
installation_id: int,
repo_full_name: str,
username: str,
required_permission: str,
) -> bool:
"""Check whether a user has required access to a repository.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
username: GitHub username to check
required_permission: Minimum required repository permission
Returns:
True if user meets or exceeds required permission, False otherwise
"""
start_time = time.time()
with create_span(
"github.get_collaborator_permission",
{
"github.repo": repo_full_name,
"github.username": username,
"github.required_permission": required_permission,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
permission = repo.get_collaborator_permission(username)
permission_order = ["none", "read", "triage", "write", "maintain", "admin"]
try:
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
permission_index = -1
required_index = 99
has_permission = permission_index >= required_index
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_permission").observe(
duration
)
github_api_requests_total.labels(operation="get_permission", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.permission": permission,
"github.has_permission": has_permission,
},
)
set_span_ok(span)
return has_permission
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:1173: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestUserHasPermission::test_user_has_permission_error - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 150 passed, 3 deselected, 1 warning in 3.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ExceptionReplacer, occurrence: 19, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1248,7 +1248,7 @@
"team": team_slug,
},
)
- except GithubException as e:
+ except CosmicRayTestingException as e:
# Team not found or no access - skip silently
logger.debug(
"Could not check team %s membership: %s",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 20, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1281,7 +1281,7 @@
return member_teams
- except Exception as e:
+ except CosmicRayTestingException as e:
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration........................................................................ [ 15%]
........................................................................ [ 31%]
............F
=================================== FAILURES ===================================
_______ TestGetUserTeamSlugs.test_get_user_team_slugs_general_exception ________
self = <stampbot.github_client.GitHubAppClient object at 0x7fa1b65665d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
> org = client.get_organization(org_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:1229:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization' id='140332522092064'>
args = ('acme',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Network error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7fa1b98720f0>
def test_get_user_team_slugs_general_exception(self):
"""Test team membership check with general exception."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_github = Mock()
mock_github.get_organization.side_effect = Exception("Network error")
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_user_team_slugs(123456, "acme", "alice", ["release-team"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1893:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa1b65665d0>
installation_id = 123456, org_name = 'acme', username = 'alice'
allowed_teams = ['release-team']
def get_user_team_slugs(
self,
installation_id: int,
org_name: str,
username: str,
allowed_teams: list[str],
) -> list[str]:
"""Get team slugs the user is a member of from the allowed teams list.
Args:
installation_id: GitHub App installation ID
org_name: Organization name
username: GitHub username to check
allowed_teams: List of team slugs to check (can be "org/team" or just "team")
Returns:
List of team slugs the user is a member of
"""
start_time = time.time()
with create_span(
"github.get_user_team_slugs",
{
"github.org": org_name,
"github.username": username,
"github.installation_id": installation_id,
"github.teams_to_check": len(allowed_teams),
},
) as span:
member_teams: list[str] = []
try:
client = self._get_installation_client(installation_id)
org = client.get_organization(org_name)
user = client.get_user(username)
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)
# Check if user is a member
if team.has_in_members(user): # type: ignore[arg-type]
member_teams.append(team_slug)
logger.debug(
"User %s is a member of team %s",
username,
team_slug,
extra={
"org": org_name,
"username": username,
"team": team_slug,
},
)
except GithubException as e:
# Team not found or no access - skip silently
logger.debug(
"Could not check team %s membership: %s",
team_slug,
e.data.get("message", str(e)) if hasattr(e, "data") else str(e),
extra={
"org": org_name,
"team": team_slug,
"username": username,
},
)
continue
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_user_teams").observe(
duration
)
github_api_requests_total.labels(operation="get_user_teams", status="success").inc()
self._update_rate_limit_metrics(client, installation_id)
add_span_attributes(
span,
{
"github.member_teams": len(member_teams),
"github.teams_checked": len(allowed_teams),
},
)
set_span_ok(span)
return member_teams
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/github_client.py:1284: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_general_exception - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 156 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -30,7 +30,7 @@ from stampbot.telemetry import add_span_attributes, create_span, set_span_error, set_span_ok # GitHub API configuration -GITHUB_API_TIMEOUT = 30 # seconds +GITHUB_API_TIMEOUT = 31 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor MAX_PRIVATE_KEY_SIZE = 64 * 1024
........................................................................ [ 15%]
........................................F
=================================== FAILURES ===================================
_ TestGetInstallationClient.test_get_installation_client_reuses_credentials_per_installation _
self = <tests.test_github_client.TestGetInstallationClient object at 0x7fe978b8ab10>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140640675838144'>
def test_get_installation_client_reuses_credentials_per_installation(
self, installation_auth_cls
):
"""Test credentials are shared per installation while each call gets its own client."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_github_cls.side_effect = lambda **kwargs: Mock(name="github", init_kwargs=kwargs)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
first = client._get_installation_client(111)
second = client._get_installation_client(111)
other = client._get_installation_client(222)
# A PyGithub client keeps in-flight request state on its connection,
# so every call gets a private client bound to shared credentials.
assert first is not second
assert other is not first
# Three calls build one client each; credentials are created once per installation.
assert mock_github_cls.call_count == 3
assert installation_auth_cls.call_args_list == [
call(mock_auth_cls.return_value, 111),
call(mock_auth_cls.return_value, 222),
]
kwargs = mock_github_cls.call_args_list[0].kwargs
assert kwargs["lazy"] is True
> assert kwargs["timeout"] == 30
E assert 31 == 30
tests/test_github_client.py:398: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_reuses_credentials_per_installation - assert 31 == 30
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 112 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 1, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -30,7 +30,7 @@ from stampbot.telemetry import add_span_attributes, create_span, set_span_error, set_span_ok # GitHub API configuration -GITHUB_API_TIMEOUT = 30 # seconds +GITHUB_API_TIMEOUT = 29 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor MAX_PRIVATE_KEY_SIZE = 64 * 1024
........................................................................ [ 15%]
........................................F
=================================== FAILURES ===================================
_ TestGetInstallationClient.test_get_installation_client_reuses_credentials_per_installation _
self = <tests.test_github_client.TestGetInstallationClient object at 0x7f68406b2b10>
installation_auth_cls = <MagicMock name='InstrumentedInstallationAuth' id='140085680289984'>
def test_get_installation_client_reuses_credentials_per_installation(
self, installation_auth_cls
):
"""Test credentials are shared per installation while each call gets its own client."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth_cls,
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_github_cls.side_effect = lambda **kwargs: Mock(name="github", init_kwargs=kwargs)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
first = client._get_installation_client(111)
second = client._get_installation_client(111)
other = client._get_installation_client(222)
# A PyGithub client keeps in-flight request state on its connection,
# so every call gets a private client bound to shared credentials.
assert first is not second
assert other is not first
# Three calls build one client each; credentials are created once per installation.
assert mock_github_cls.call_count == 3
assert installation_auth_cls.call_args_list == [
call(mock_auth_cls.return_value, 111),
call(mock_auth_cls.return_value, 222),
]
kwargs = mock_github_cls.call_args_list[0].kwargs
assert kwargs["lazy"] is True
> assert kwargs["timeout"] == 30
E assert 29 == 30
tests/test_github_client.py:398: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetInstallationClient::test_get_installation_client_reuses_credentials_per_installation - assert 29 == 30
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 112 passed, 3 deselected, 1 warning in 2.83s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 2, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -31,7 +31,7 @@ # GitHub API configuration GITHUB_API_TIMEOUT = 30 # seconds -GITHUB_API_RETRY_TOTAL = 3 +GITHUB_API_RETRY_TOTAL = 4 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor MAX_PRIVATE_KEY_SIZE = 64 * 1024
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.14s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 3, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -31,7 +31,7 @@ # GitHub API configuration GITHUB_API_TIMEOUT = 30 # seconds -GITHUB_API_RETRY_TOTAL = 3 +GITHUB_API_RETRY_TOTAL = 2 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor MAX_PRIVATE_KEY_SIZE = 64 * 1024
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 4, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -32,7 +32,7 @@ # GitHub API configuration GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 -GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor +GITHUB_API_RETRY_BACKOFF = 1.5 # exponential backoff factor MAX_PRIVATE_KEY_SIZE = 64 * 1024 # Installation credentials hold a token that PyGithub refreshes shortly before
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 5, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -32,7 +32,7 @@ # GitHub API configuration GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 -GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor +GITHUB_API_RETRY_BACKOFF = -0.5 # exponential backoff factor MAX_PRIVATE_KEY_SIZE = 64 * 1024 # Installation credentials hold a token that PyGithub refreshes shortly before
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.42s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 6, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 65 * 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.15s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 7, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 63 * 1024 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 8, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 * 1025 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 9, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -33,7 +33,7 @@ GITHUB_API_TIMEOUT = 30 # seconds GITHUB_API_RETRY_TOTAL = 3 GITHUB_API_RETRY_BACKOFF = 0.5 # exponential backoff factor -MAX_PRIVATE_KEY_SIZE = 64 * 1024 +MAX_PRIVATE_KEY_SIZE = 64 * 1023 # Installation credentials hold a token that PyGithub refreshes shortly before # GitHub expires it. Entries expire an hour after creation, which bounds memory
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 10, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -40,7 +40,7 @@ # for installations that disappear and rotates the token about once an hour. # The size also bounds the installation_id label on the rate-limit gauges, which # keeps those metrics inside the repository's low-cardinality rule. -INSTALLATION_AUTH_CACHE_SIZE = 100 +INSTALLATION_AUTH_CACHE_SIZE = 101 INSTALLATION_AUTH_CACHE_TTL = 3600 # seconds # The App slug only changes when an operator renames the App. BOT_LOGIN_TTL = 3600 # seconds
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 11, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -40,7 +40,7 @@ # for installations that disappear and rotates the token about once an hour. # The size also bounds the installation_id label on the rate-limit gauges, which # keeps those metrics inside the repository's low-cardinality rule. -INSTALLATION_AUTH_CACHE_SIZE = 100 +INSTALLATION_AUTH_CACHE_SIZE = 99 INSTALLATION_AUTH_CACHE_TTL = 3600 # seconds # The App slug only changes when an operator renames the App. BOT_LOGIN_TTL = 3600 # seconds
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 12, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -41,7 +41,7 @@ # The size also bounds the installation_id label on the rate-limit gauges, which # keeps those metrics inside the repository's low-cardinality rule. INSTALLATION_AUTH_CACHE_SIZE = 100 -INSTALLATION_AUTH_CACHE_TTL = 3600 # seconds +INSTALLATION_AUTH_CACHE_TTL = 3601 # seconds # The App slug only changes when an operator renames the App. BOT_LOGIN_TTL = 3600 # seconds
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 13, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -41,7 +41,7 @@ # The size also bounds the installation_id label on the rate-limit gauges, which # keeps those metrics inside the repository's low-cardinality rule. INSTALLATION_AUTH_CACHE_SIZE = 100 -INSTALLATION_AUTH_CACHE_TTL = 3600 # seconds +INSTALLATION_AUTH_CACHE_TTL = 3599 # seconds # The App slug only changes when an operator renames the App. BOT_LOGIN_TTL = 3600 # seconds
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.24s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 14, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -43,7 +43,7 @@ INSTALLATION_AUTH_CACHE_SIZE = 100 INSTALLATION_AUTH_CACHE_TTL = 3600 # seconds # The App slug only changes when an operator renames the App. -BOT_LOGIN_TTL = 3600 # seconds +BOT_LOGIN_TTL = 3601 # seconds logger = get_logger(__name__)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 15, definition_name: None
--- mutation diff --- --- a/stampbot/github_client.py +++ b/stampbot/github_client.py @@ -43,7 +43,7 @@ INSTALLATION_AUTH_CACHE_SIZE = 100 INSTALLATION_AUTH_CACHE_TTL = 3600 # seconds # The App slug only changes when an operator renames the App. -BOT_LOGIN_TTL = 3600 # seconds +BOT_LOGIN_TTL = 3599 # seconds logger = get_logger(__name__)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 16, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -102,7 +102,7 @@
try:
repo.complete()
except GithubException as e:
- return e.status == 404
+ return e.status == 405
except Exception:
return False
return False........................................................................ [ 15%]
...................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_accepts_missing_optional_repository_with_lazy_client _
self = <tests.test_github_client.TestGetRepoFile object at 0x7fde41865400>
def test_get_repo_file_accepts_missing_optional_repository_with_lazy_client(self):
"""Test a lazy client confirms the optional repository itself is missing."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_integration_cls.return_value = mock_integration
# Lazy get_repo never raises. The file read and the root read both
# 404, and only the repository request proves the repo is absent.
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
GithubException(404, {"message": "Not Found"}, None),
]
mock_repo.complete.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/.github",
"stampbot.toml",
missing_repository_is_optional=True,
)
tests/test_github_client.py:1236:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140592501620816'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:02:36.986563Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/.github (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/.github (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:02:36.986563Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_accepts_missing_optional_repository_with_lazy_client - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 139 passed, 3 deselected, 1 warning in 3.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 17, definition_name: _repository_is_missing
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -102,7 +102,7 @@
try:
repo.complete()
except GithubException as e:
- return e.status == 404
+ return e.status == 403
except Exception:
return False
return False........................................................................ [ 15%]
...................................................................F
=================================== FAILURES ===================================
_ TestGetRepoFile.test_get_repo_file_accepts_missing_optional_repository_with_lazy_client _
self = <tests.test_github_client.TestGetRepoFile object at 0x7f1cc55e1400>
def test_get_repo_file_accepts_missing_optional_repository_with_lazy_client(self):
"""Test a lazy client confirms the optional repository itself is missing."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_integration_cls.return_value = mock_integration
# Lazy get_repo never raises. The file read and the root read both
# 404, and only the repository request proves the repo is absent.
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
GithubException(404, {"message": "Not Found"}, None),
]
mock_repo.complete.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(
123456,
"owner/.github",
"stampbot.toml",
missing_repository_is_optional=True,
)
tests/test_github_client.py:1236:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139761489330256'>
args = ('stampbot.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T23:01:20.894756Z[0m [[33m[1mwarning [0m] [1mGitHub could not read stampbot.toml from owner/.github (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/.github', 'file_path': 'stampbot.toml', 'installation_id': 123456}, 'event': 'GitHub could not read stampbot.toml from owner/.github (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T23:01:20.894756Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_accepts_missing_optional_repository_with_lazy_client - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 139 passed, 3 deselected, 1 warning in 3.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 18, definition_name: _build_retry
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -131,7 +131,7 @@
return Retry(
total=GITHUB_API_RETRY_TOTAL,
backoff_factor=GITHUB_API_RETRY_BACKOFF,
- status_forcelist=[500, 502, 503, 504],
+ status_forcelist=[ 501, 502, 503, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 19, definition_name: _build_retry
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -131,7 +131,7 @@
return Retry(
total=GITHUB_API_RETRY_TOTAL,
backoff_factor=GITHUB_API_RETRY_BACKOFF,
- status_forcelist=[500, 502, 503, 504],
+ status_forcelist=[ 499, 502, 503, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.23s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 20, definition_name: _build_retry
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -131,7 +131,7 @@
return Retry(
total=GITHUB_API_RETRY_TOTAL,
backoff_factor=GITHUB_API_RETRY_BACKOFF,
- status_forcelist=[500, 502, 503, 504],
+ status_forcelist=[500, 503, 503, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.14s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 21, definition_name: _build_retry
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -131,7 +131,7 @@
return Retry(
total=GITHUB_API_RETRY_TOTAL,
backoff_factor=GITHUB_API_RETRY_BACKOFF,
- status_forcelist=[500, 502, 503, 504],
+ status_forcelist=[500, 501, 503, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 22, definition_name: _build_retry
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -131,7 +131,7 @@
return Retry(
total=GITHUB_API_RETRY_TOTAL,
backoff_factor=GITHUB_API_RETRY_BACKOFF,
- status_forcelist=[500, 502, 503, 504],
+ status_forcelist=[500, 502, 504, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 23, definition_name: _build_retry
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -131,7 +131,7 @@
return Retry(
total=GITHUB_API_RETRY_TOTAL,
backoff_factor=GITHUB_API_RETRY_BACKOFF,
- status_forcelist=[500, 502, 503, 504],
+ status_forcelist=[500, 502, 502, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 24, definition_name: _build_retry
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -131,7 +131,7 @@
return Retry(
total=GITHUB_API_RETRY_TOTAL,
backoff_factor=GITHUB_API_RETRY_BACKOFF,
- status_forcelist=[500, 502, 503, 504],
+ status_forcelist=[500, 502, 503, 505],
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 25, definition_name: _build_retry
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -131,7 +131,7 @@
return Retry(
total=GITHUB_API_RETRY_TOTAL,
backoff_factor=GITHUB_API_RETRY_BACKOFF,
- status_forcelist=[500, 502, 503, 504],
+ status_forcelist=[500, 502, 503, 503],
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 26, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 2)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 27, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -238,7 +238,7 @@
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
- pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
+ pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 0)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 28, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) < 4 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f89b6aeb770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f89b21a28b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 4 or not pem_lines[0].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.82s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 29, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) < 2 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 30, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) < 3 or not pem_lines[ 1].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f4d68ccb770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f4d6438a8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[ 1].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 31, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -251,7 +251,7 @@
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
- if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
+ if len(pem_lines) < 3 or not pem_lines[ -1].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fdb74ed3770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fdb705a28b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[ -1].startswith("-----BEGIN "):
> raise ValueError("Private key must be in PEM format")
E ValueError: Private key must be in PEM format
stampbot/github_client.py:255: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key must be in PEM format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 32, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -253,7 +253,7 @@
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
- if not pem_lines[0].endswith("PRIVATE KEY-----"):
+ if not pem_lines[ 1].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f9d10c93770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f9d0c38a8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[ 1].endswith("PRIVATE KEY-----"):
> raise ValueError("PEM value must contain a private key")
E ValueError: PEM value must contain a private key
stampbot/github_client.py:257: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: PEM value must contain a private key
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.85s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 33, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -253,7 +253,7 @@
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
- if not pem_lines[0].endswith("PRIVATE KEY-----"):
+ if not pem_lines[ -1].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 34, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -255,7 +255,7 @@
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
- expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
+ expected_footer = pem_lines[ 1].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f26428c7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f263df4e8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[ 1].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 35, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -255,7 +255,7 @@
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
- expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
+ expected_footer = pem_lines[ -1].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):........................................................................ [ 15%]
..................................F
=================================== FAILURES ===================================
______ TestLoadPrivateKey.test_load_private_key_rejects_mismatched_footer ______
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7f86c0495d60>
def test_load_private_key_rejects_mismatched_footer(self):
"""Test the PEM header and footer must describe the same key type."""
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = (
"-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----"
)
from stampbot.github_client import GitHubAppClient
> with pytest.raises(ValueError, match="footer does not match"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_github_client.py:271: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_rejects_mismatched_footer - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 106 passed, 3 deselected, 1 warning in 2.76s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 36, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -255,7 +255,7 @@
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
- expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
+ expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 2)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 37, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -255,7 +255,7 @@
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
- expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
+ expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 0)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f7faa673770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f7fa5c6e8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 0)
if pem_lines[-1] != expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 38, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[- 2] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f57558e3770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f5750f968b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[- 2] != expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 39, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -256,7 +256,7 @@
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
- if pem_lines[-1] != expected_footer:
+ if pem_lines[- 0] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:-1]):
raise ValueError("Private key PEM body is empty")........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fa492ae3770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fa48e17e8b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[- 0] != expected_footer:
> raise ValueError("Private key PEM footer does not match its header")
E ValueError: Private key PEM footer does not match its header
stampbot/github_client.py:260: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM footer does not match its header
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 40, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if not any(line.strip() for line in pem_lines[ 2:-1]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7fe00d3ab770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7fe0089c68b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[ 2:-1]):
> raise ValueError("Private key PEM body is empty")
E ValueError: Private key PEM body is empty
stampbot/github_client.py:262: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM body is empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 41, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if not any(line.strip() for line in pem_lines[ 0:-1]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.....................................F
=================================== FAILURES ===================================
_______ TestLoadPrivateKey.test_load_private_key_rejects_empty_pem_body ________
self = <tests.test_github_client.TestLoadPrivateKey object at 0x7fb444126890>
def test_load_private_key_rejects_empty_pem_body(self):
"""Test a private-key envelope must contain body data."""
with patch("stampbot.github_client.settings") as mock_settings:
mock_settings.private_key = TEST_PEM_KEY.replace("test", "")
from stampbot.github_client import GitHubAppClient
> with pytest.raises(ValueError, match="body is empty"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_github_client.py:303: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestLoadPrivateKey::test_load_private_key_rejects_empty_pem_body - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 109 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 42, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if not any(line.strip() for line in pem_lines[1:- 2]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7ff2ce7ab770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7ff2c9d828b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:- 2]):
> raise ValueError("Private key PEM body is empty")
E ValueError: Private key PEM body is empty
stampbot/github_client.py:262: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM body is empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 43, definition_name: _load_private_key
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -258,7 +258,7 @@
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
- if not any(line.strip() for line in pem_lines[1:-1]):
+ if not any(line.strip() for line in pem_lines[1:- 0]):
raise ValueError("Private key PEM body is empty")
return pem_content........................................................................ [ 15%]
.......................F
=================================== FAILURES ===================================
____________ TestEnsureInitialized.test_ensure_initialized_success _____________
self = <tests.test_github_client.TestEnsureInitialized object at 0x7f4802ad7770>
def test_ensure_initialized_success(self):
"""Test successful initialization."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth") as mock_auth,
patch("stampbot.github_client.GithubIntegration") as mock_integration,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> client._ensure_initialized()
tests/test_github_client.py:114:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:188: in _ensure_initialized
private_key = self._load_private_key()
^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.github_client.GitHubAppClient object at 0x7f47fe1d28b0>
def _load_private_key(self) -> str:
"""Load private key from settings or file.
Returns:
Private key contents as PEM string.
Raises:
RuntimeError: If private key is not configured.
ValueError: If private key is not valid PEM format.
OSError: If private key file cannot be read.
"""
key: str | None = settings.private_key
if key is None:
raise RuntimeError("Private key not configured")
# A non-PEM value is an operator-selected file path. It is not
# request-controlled, so arbitrary relative and absolute paths are
# intentional. Validate the opened target instead of pretending that
# normalizing '..' segments creates a meaningful traversal boundary.
if key.startswith("-----BEGIN"):
pem_content = key
else:
try:
with Path(key).expanduser().open(encoding="utf-8") as key_file:
file_metadata = os.fstat(key_file.fileno())
if not stat.S_ISREG(file_metadata.st_mode):
raise ValueError("Private key path must reference a regular file")
if file_metadata.st_size > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key file exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
pem_content = key_file.read(MAX_PRIVATE_KEY_SIZE + 1)
except FileNotFoundError:
raise ValueError("Invalid private key path: file does not exist") from None
except OSError as e:
logger.error("Failed to read private key file: %s", e)
raise
if len(pem_content.encode("utf-8")) > MAX_PRIVATE_KEY_SIZE:
raise ValueError(f"Private key exceeds {MAX_PRIVATE_KEY_SIZE} bytes")
# Validate a complete PEM private-key envelope. PyGithub performs the
# cryptographic parsing when it creates AppAuth.
pem_lines = pem_content.strip().splitlines()
if len(pem_lines) < 3 or not pem_lines[0].startswith("-----BEGIN "):
raise ValueError("Private key must be in PEM format")
if not pem_lines[0].endswith("PRIVATE KEY-----"):
raise ValueError("PEM value must contain a private key")
expected_footer = pem_lines[0].replace("-----BEGIN ", "-----END ", 1)
if pem_lines[-1] != expected_footer:
raise ValueError("Private key PEM footer does not match its header")
if not any(line.strip() for line in pem_lines[1:- 0]):
> raise ValueError("Private key PEM body is empty")
E ValueError: Private key PEM body is empty
stampbot/github_client.py:262: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestEnsureInitialized::test_ensure_initialized_success - ValueError: Private key PEM body is empty
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 95 passed, 3 deselected, 1 warning in 2.83s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 44, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < 1 or limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 45, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < -1 or limit < 0:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 46, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < 0 or limit < 1:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 47, definition_name: _update_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -370,7 +370,7 @@
"""
try:
remaining, limit = client.requester.rate_limiting
- if remaining < 0 or limit < 0:
+ if remaining < 0 or limit < -1:
# PyGithub reports -1 until a response has carried the headers.
return
label = str(installation_id)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.28s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 48, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None and self._bot_login_value[ 2] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 49, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -394,7 +394,7 @@
"""
with self._bot_login_lock:
now = time.monotonic()
- if self._bot_login_value is not None and self._bot_login_value[1] > now:
+ if self._bot_login_value is not None and self._bot_login_value[ 0] > now:
return self._bot_login_value[0]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 50, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -395,7 +395,7 @@
with self._bot_login_lock:
now = time.monotonic()
if self._bot_login_value is not None and self._bot_login_value[1] > now:
- return self._bot_login_value[0]
+ return self._bot_login_value[ 1]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)
return login........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 51, definition_name: _bot_login
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -395,7 +395,7 @@
with self._bot_login_lock:
now = time.monotonic()
if self._bot_login_value is not None and self._bot_login_value[1] > now:
- return self._bot_login_value[0]
+ return self._bot_login_value[ -1]
login = f"{self.integration.get_app().slug}[bot]"
self._bot_login_value = (login, now + BOT_LOGIN_TTL)
return login........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.20s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 52, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 423 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7f9317dec7d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f9314b9a7b0>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:34:07.889452Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:34:07.889452Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7f9314b9a7b0>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 2.98s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 53, definition_name: dismiss_approval
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -585,7 +585,7 @@
# which two replicas cleaning up the same duplicate reach on
# purpose. 422 also covers other validation failures, so the
# review's state is read back before treating it as done.
- if e.status != 422 or pr.get_review(review_id).state != "DISMISSED":
+ if e.status != 421 or pr.get_review(review_id).state != "DISMISSED":
raise
logger.info(
"Review %d on PR #%d in %s was already dismissed",........................................................................ [ 15%]
......................................................F
=================================== FAILURES ===================================
____ TestDismissApproval.test_dismiss_approval_already_dismissed_is_success ____
self = <tests.test_github_client.TestDismissApproval object at 0x7faa16b907d0>
def test_dismiss_approval_already_dismissed_is_success(self):
"""Test GitHub's 422 for an already dismissed review counts as done."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration"),
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_review = Mock()
mock_review.state = "DISMISSED"
mock_review.dismiss.side_effect = GithubException(
422, {"message": "Can not dismiss a dismissed pull request review"}, None
)
mock_pr = Mock()
mock_pr.get_review.return_value = mock_review
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> assert client.dismiss_approval(123456, "owner/repo", 42, 789, "Duplicate") is True
E AssertionError: assert False is True
E + where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
E + where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7faa13a3e850>.dismiss_approval
tests/test_github_client.py:828: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:54:28.139626Z[0m [[31m[1merror [0m] [1mFailed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:629 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': '422 {"message": "Can not dismiss a dismissed pull request review"}'}, 'event': 'Failed to dismiss approval for PR #42 in owner/repo: 422 {"message": "Can not dismiss a dismissed pull request review"}', 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T20:54:28.139626Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestDismissApproval::test_dismiss_approval_already_dismissed_is_success - AssertionError: assert False is True
+ where False = dismiss_approval(123456, 'owner/repo', 42, 789, 'Duplicate')
+ where dismiss_approval = <stampbot.github_client.GitHubAppClient object at 0x7faa13a3e850>.dismiss_approval
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 126 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 54, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if e.status == 405 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f91af91f100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='140263634367696'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T20:58:55.435016Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T20:58:55.435016Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 55, definition_name: get_repo_file
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -706,7 +706,7 @@
duration = time.time() - start_time
github_api_request_duration_seconds.labels(operation="get_file").observe(duration)
not_found: NotFoundKind = "unconfirmed"
- if e.status == 404 and repo is not None:
+ if e.status == 403 and repo is not None:
not_found = _classify_not_found(
repo, ref, optional_repo=missing_repository_is_optional
)........................................................................ [ 15%]
............................................................F
=================================== FAILURES ===================================
_________ TestGetRepoFile.test_get_repo_file_returns_none_on_not_found _________
self = <tests.test_github_client.TestGetRepoFile object at 0x7f0bfdb83100>
def test_get_repo_file_returns_none_on_not_found(self):
"""Test that a clean GitHub not-found result advances policy lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_contents.side_effect = [
GithubException(404, {"message": "Not Found"}, None),
[],
]
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.get_repo_file(123456, "owner/repo", "nonexistent.toml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:1060:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:683: in get_repo_file
content = _get_repo_contents(repo, file_path, ref)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/github_client.py:82: in _get_repo_contents
return repo.get_contents(path)
^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_contents' id='139689416815824'>
args = ('nonexistent.toml',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
> raise result
E github.GithubException.GithubException: 404 {"message": "Not Found"}
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1245: GithubException
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T21:17:59.413045Z[0m [[33m[1mwarning [0m] [1mGitHub could not read nonexistent.toml from owner/repo (status 404)[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35mwarning[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}[0m
------------------------------ Captured log call -------------------------------
WARNING stampbot.github_client:github_client.py:747 {'extra': {'repo': 'owner/repo', 'file_path': 'nonexistent.toml', 'installation_id': 123456}, 'event': 'GitHub could not read nonexistent.toml from owner/repo (status 404)', 'logger': 'stampbot.github_client', 'level': 'warning', 'timestamp': '2026-09-04T21:17:59.413045Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetRepoFile::test_get_repo_file_returns_none_on_not_found - github.GithubException.GithubException: 404 {"message": "Not Found"}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 132 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 56, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1067,7 +1067,7 @@
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
- if e.status == 404:
+ if e.status == 405:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f57497c1450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "missing")
> assert result is False
E assert None is False
tests/test_github_client.py:1977: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - assert None is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 2.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 57, definition_name: repo_has_label
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1067,7 +1067,7 @@
github_api_request_duration_seconds.labels(operation="get_label").observe(duration)
github_api_requests_total.labels(operation="get_label", status="failure").inc()
- if e.status == 404:
+ if e.status == 403:
add_span_attributes(span, {"github.label_found": False})
set_span_ok(span)
return False........................................................................ [ 15%]
........................................................................ [ 31%]
..............F
=================================== FAILURES ===================================
________________ TestRepoHasLabel.test_repo_has_label_not_found ________________
self = <tests.test_github_client.TestRepoHasLabel object at 0x7f9a55c85450>
def test_repo_has_label_not_found(self):
"""Test repo_has_label returns False when label is missing."""
from github.GithubException import GithubException
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_repo = Mock()
mock_repo.get_label.side_effect = GithubException(404, {"message": "Not Found"}, None)
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.repo_has_label(123456, "owner/repo", "missing")
> assert result is False
E assert None is False
tests/test_github_client.py:1977: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRepoHasLabel::test_repo_has_label_not_found - assert None is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 158 passed, 3 deselected, 1 warning in 2.92s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 58, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1146,7 +1146,7 @@
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
- permission_index = -1
+ permission_index = - 2
required_index = 99
has_permission = permission_index >= required_index........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 59, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1146,7 +1146,7 @@
permission_index = permission_order.index(permission)
required_index = permission_order.index(required_permission)
except ValueError:
- permission_index = -1
+ permission_index = - 0
required_index = 99
has_permission = permission_index >= required_index........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 60, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1147,7 +1147,7 @@
required_index = permission_order.index(required_permission)
except ValueError:
permission_index = -1
- required_index = 99
+ required_index = 100
has_permission = permission_index >= required_index
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 61, definition_name: user_has_permission
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1147,7 +1147,7 @@
required_index = permission_order.index(required_permission)
except ValueError:
permission_index = -1
- required_index = 99
+ required_index = 98
has_permission = permission_index >= required_index
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 62, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1231,7 +1231,7 @@
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
- team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
+ team_slug = team_ref.split("/")[- 2] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)........................................................................ [ 15%]
........................................................................ [ 31%]
.........F
=================================== FAILURES ===================================
________ TestGetUserTeamSlugs.test_get_user_team_slugs_with_org_prefix _________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f4f13a7d1d0>
def test_get_user_team_slugs_with_org_prefix(self):
"""Test team membership check with org/team format."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_user = Mock()
mock_team = Mock()
mock_team.has_in_members.return_value = True
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(123456, "acme", "alice", ["acme/release-team"])
# Should extract "release-team" from "acme/release-team"
> mock_org.get_team_by_slug.assert_called_with("release-team")
tests/test_github_client.py:1775:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization().get_team_by_slug' id='139977553511824'>
args = ('release-team',), kwargs = {}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: get_team_by_slug('release-team')
E Actual: get_team_by_slug('acme')
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_with_org_prefix - AssertionError: expected call not found.
Expected: get_team_by_slug('release-team')
Actual: get_team_by_slug('acme')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 153 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 63, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1231,7 +1231,7 @@
for team_ref in allowed_teams:
# Extract team slug (handle both "org/team" and "team" formats)
- team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
+ team_slug = team_ref.split("/")[- 0] if "/" in team_ref else team_ref
try:
team = org.get_team_by_slug(team_slug)........................................................................ [ 15%]
........................................................................ [ 31%]
.........F
=================================== FAILURES ===================================
________ TestGetUserTeamSlugs.test_get_user_team_slugs_with_org_prefix _________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7fb6bf4811d0>
def test_get_user_team_slugs_with_org_prefix(self):
"""Test team membership check with org/team format."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_user = Mock()
mock_team = Mock()
mock_team.has_in_members.return_value = True
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(123456, "acme", "alice", ["acme/release-team"])
# Should extract "release-team" from "acme/release-team"
> mock_org.get_team_by_slug.assert_called_with("release-team")
tests/test_github_client.py:1775:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_organization().get_team_by_slug' id='140422746408336'>
args = ('release-team',), kwargs = {}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: get_team_by_slug('release-team')
E Actual: get_team_by_slug('acme')
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_with_org_prefix - AssertionError: expected call not found.
Expected: get_team_by_slug('release-team')
Actual: get_team_by_slug('acme')
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 153 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 0, definition_name: GitHubAppClient
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -193,8 +193,6 @@
retry=_build_retry(),
)
self._initialized = True
-
- @property
def integration(self) -> GithubIntegration:
"""Get GitHub integration, initializing if needed.
........................................................................ [ 15%]
........................F
=================================== FAILURES ===================================
_____ TestIntegrationProperty.test_integration_raises_when_not_initialized _____
self = <tests.test_github_client.TestIntegrationProperty object at 0x7f240d54f610>
def test_integration_raises_when_not_initialized(self):
"""Test that integration property raises when not initialized after _ensure_initialized."""
with patch("stampbot.github_client.is_configured", return_value=False):
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> with pytest.raises(RuntimeError):
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE RuntimeError
tests/test_github_client.py:130: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestIntegrationProperty::test_integration_raises_when_not_initialized - Failed: DID NOT RAISE RuntimeError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 96 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 0, definition_name: _prune_rate_limit_metrics
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -322,7 +322,7 @@
limit. Callers hold ``_installation_auths_lock``.
"""
live = {str(installation_id) for installation_id in self._installation_auths}
- for stale in self._rate_limit_installations - live:
+ for stale in []:
# remove() is a no-op for a label set the gauge never held.
github_api_rate_limit_remaining.remove(stale)
github_api_rate_limit_limit.remove(stale)........................................................................ [ 15%]
............................................F
=================================== FAILURES ===================================
_ TestRateLimitLabelPruning.test_prune_removes_labels_for_evicted_installations _
self = <tests.test_github_client.TestRateLimitLabelPruning object at 0x7feaeae9fc50>
def test_prune_removes_labels_for_evicted_installations(self):
"""Test gauge label sets follow the credential cache, so churn cannot grow them."""
from stampbot.github_client import GitHubAppClient
from stampbot.metrics import github_api_rate_limit_limit, github_api_rate_limit_remaining
client = GitHubAppClient()
for installation_id in (9001, 9002):
fake = Mock()
fake.requester.rate_limiting = (10, 5000)
client._update_rate_limit_metrics(fake, installation_id)
assert client._rate_limit_installations == {"9001", "9002"}
# Only 9002 still has live credentials.
client._installation_auths[9002] = Mock()
with client._installation_auths_lock:
client._prune_rate_limit_metrics()
assert client._rate_limit_installations == {"9002"}
for gauge in (github_api_rate_limit_remaining, github_api_rate_limit_limit):
labelled = {sample.labels["installation_id"] for sample in gauge.collect()[0].samples}
> assert "9001" not in labelled
E AssertionError: assert '9001' not in {'9001', '9002'}
tests/test_github_client.py:529: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestRateLimitLabelPruning::test_prune_removes_labels_for_evicted_installations - AssertionError: assert '9001' not in {'9001', '9002'}
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 116 passed, 3 deselected, 1 warning in 2.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 1, definition_name: find_bot_approval_reviews
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -811,7 +811,7 @@
bot_user = self._bot_login()
bot_reviews: list[BotReview] = []
- for review in pr.get_reviews():
+ for review in []:
if review.user.login == bot_user and review.state in (
"APPROVED",
"DISMISSED",........................................................................ [ 15%]
........................................................................ [ 31%]
..F
=================================== FAILURES ===================================
______ TestFindBotApprovalReviews.test_find_bot_approval_reviews_success _______
self = <tests.test_github_client.TestFindBotApprovalReviews object at 0x7ff0657e0b90>
def test_find_bot_approval_reviews_success(self):
"""Test successful bot approval review state lookup."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration.get_app.return_value = Mock(slug="stampbot")
mock_integration_cls.return_value = mock_integration
bot_approved_review = Mock()
bot_approved_review.user.login = "stampbot[bot]"
bot_approved_review.state = "APPROVED"
bot_approved_review.id = 123
bot_approved_review.commit_id = "headsha"
bot_dismissed_review = Mock()
bot_dismissed_review.user.login = "stampbot[bot]"
bot_dismissed_review.state = "DISMISSED"
bot_dismissed_review.id = 124
bot_dismissed_review.commit_id = "oldsha"
bot_commented_review = Mock()
bot_commented_review.user.login = "stampbot[bot]"
bot_commented_review.state = "COMMENTED"
bot_commented_review.id = 125
other_review = Mock()
other_review.user.login = "other-user"
other_review.state = "APPROVED"
other_review.id = 456
mock_pr = Mock()
mock_pr.get_reviews.return_value = [
bot_approved_review,
bot_dismissed_review,
bot_commented_review,
other_review,
]
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.find_bot_approval_reviews(123456, "owner/repo", 42)
> assert result == [
{"id": 123, "state": "APPROVED", "commit_id": "headsha"},
{"id": 124, "state": "DISMISSED", "commit_id": "oldsha"},
]
E AssertionError: assert [] == [{'id': 123, ...d': 'oldsha'}]
E
E Right contains 2 more items, first extra item: {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
E
E Full diff:
E + []
E - [
E - {
E - 'id': 123,
E - 'state': 'APPROVED',
E - 'commit_id': 'headsha',
E - },
E - {
E - 'id': 124,
E - 'state': 'DISMISSED',
E - 'commit_id': 'oldsha',
E - },
E - ]
tests/test_github_client.py:1481: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestFindBotApprovalReviews::test_find_bot_approval_reviews_success - AssertionError: assert [] == [{'id': 123, ...d': 'oldsha'}]
Right contains 2 more items, first extra item: {'id': 123, 'state': 'APPROVED', 'commit_id': 'headsha'}
Full diff:
+ []
- [
- {
- 'id': 123,
- 'state': 'APPROVED',
- 'commit_id': 'headsha',
- },
- {
- 'id': 124,
- 'state': 'DISMISSED',
- 'commit_id': 'oldsha',
- },
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 146 passed, 3 deselected, 1 warning in 2.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 2, definition_name: get_user_team_slugs
--- mutation diff ---
--- a/stampbot/github_client.py
+++ b/stampbot/github_client.py
@@ -1229,7 +1229,7 @@
org = client.get_organization(org_name)
user = client.get_user(username)
- for team_ref in allowed_teams:
+ for team_ref in []:
# Extract team slug (handle both "org/team" and "team" formats)
team_slug = team_ref.split("/")[-1] if "/" in team_ref else team_ref
........................................................................ [ 15%]
........................................................................ [ 31%]
........F
=================================== FAILURES ===================================
____________ TestGetUserTeamSlugs.test_get_user_team_slugs_success _____________
self = <tests.test_github_client.TestGetUserTeamSlugs object at 0x7f68d6a81090>
def test_get_user_team_slugs_success(self):
"""Test successful team membership check."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
# Mock user
mock_user = Mock()
mock_user.login = "alice"
# Mock team that has alice as member
mock_team = Mock()
mock_team.has_in_members.return_value = True
# Mock org
mock_org = Mock()
mock_org.get_team_by_slug.return_value = mock_team
mock_github = Mock()
mock_github.get_organization.return_value = mock_org
mock_github.get_user.return_value = mock_user
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.get_user_team_slugs(
123456, "acme", "alice", ["release-team", "deploy-team"]
)
> assert result == ["release-team", "deploy-team"]
E AssertionError: assert [] == ['release-tea...'deploy-team']
E
E Right contains 2 more items, first extra item: 'release-team'
E
E Full diff:
E + []
E - [
E - 'release-team',
E - 'deploy-team',
E - ]
tests/test_github_client.py:1731: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestGetUserTeamSlugs::test_get_user_team_slugs_success - AssertionError: assert [] == ['release-tea...'deploy-team']
Right contains 2 more items, first extra item: 'release-team'
Full diff:
+ []
- [
- 'release-team',
- 'deploy-team',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 152 passed, 3 deselected, 1 warning in 2.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 0, definition_name: _resolve_app_version
--- mutation diff ---
--- a/stampbot/version.py
+++ b/stampbot/version.py
@@ -15,7 +15,7 @@
def _resolve_app_version() -> str:
"""Return build-injected, installed-package, or unknown version metadata."""
injected_version = os.getenv(_VERSION_ENVIRONMENT_VARIABLE, "").strip()
- if injected_version:
+ if not injected_version:
return injected_version
try:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:182: in <module>
app = FastAPI(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/applications.py:928: in __init__
assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'"
^^^^^^^^^^^^
E AssertionError: A version must be provided for OpenAPI, e.g.: '2.1.0'
------------------------------- Captured stdout --------------------------------
[2m2026-09-04T21:07:32.621360Z[0m [[32m[1minfo [0m] [1mOpenTelemetry disabled [0m [[0m[1m[34mstampbot.telemetry[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.telemetry (INFO)>[0m [36m_name[0m=[35minfo[0m
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - AssertionError: A version must be provided for OpenAPI, e.g.: '2.1.0'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.27s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 0, definition_name: _resolve_app_version
--- mutation diff ---
--- a/stampbot/version.py
+++ b/stampbot/version.py
@@ -23,7 +23,7 @@
except PackageNotFoundError:
return _UNKNOWN_VERSION
- return installed_version or _UNKNOWN_VERSION
+ return installed_version and _UNKNOWN_VERSION
APP_VERSION = _resolve_app_version()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
....................................................F
=================================== FAILURES ===================================
_____________ test_installed_distribution_is_development_fallback ______________
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7fed3ceed550>
def test_installed_distribution_is_development_fallback(monkeypatch):
monkeypatch.delenv("STAMPBOT_VERSION", raising=False)
with patch("stampbot.version.distribution_version", return_value="1.2.3"):
> assert _resolve_app_version() == "1.2.3"
E AssertionError: assert '0.0.0+unknown' == '1.2.3'
E
E - 1.2.3
E + 0.0.0+unknown
tests/test_version.py:22: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_version.py::test_installed_distribution_is_development_fallback - AssertionError: assert '0.0.0+unknown' == '1.2.3'
- 1.2.3
+ 0.0.0+unknown
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 340 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 0, definition_name: _resolve_app_version
--- mutation diff ---
--- a/stampbot/version.py
+++ b/stampbot/version.py
@@ -20,7 +20,7 @@
try:
installed_version = distribution_version(_DISTRIBUTION_NAME).strip()
- except PackageNotFoundError:
+ except CosmicRayTestingException:
return _UNKNOWN_VERSION
return installed_version or _UNKNOWN_VERSION........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.....................................................F
=================================== FAILURES ===================================
_________ test_unknown_fallback_without_build_or_distribution_version __________
def _resolve_app_version() -> str:
"""Return build-injected, installed-package, or unknown version metadata."""
injected_version = os.getenv(_VERSION_ENVIRONMENT_VARIABLE, "").strip()
if injected_version:
return injected_version
try:
> installed_version = distribution_version(_DISTRIBUTION_NAME).strip()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/version.py:22:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='distribution_version' id='140487958338864'>
args = ('stampbot',), kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E importlib.metadata.PackageNotFoundError: <exception str() failed>
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: PackageNotFoundError
During handling of the above exception, another exception occurred:
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7fc5e7384f50>
def test_unknown_fallback_without_build_or_distribution_version(monkeypatch):
monkeypatch.setenv("STAMPBOT_VERSION", " ")
with patch(
"stampbot.version.distribution_version",
side_effect=PackageNotFoundError,
):
> assert _resolve_app_version() == "0.0.0+unknown"
^^^^^^^^^^^^^^^^^^^^^^
tests/test_version.py:32:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def _resolve_app_version() -> str:
"""Return build-injected, installed-package, or unknown version metadata."""
injected_version = os.getenv(_VERSION_ENVIRONMENT_VARIABLE, "").strip()
if injected_version:
return injected_version
try:
installed_version = distribution_version(_DISTRIBUTION_NAME).strip()
> except CosmicRayTestingException:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/version.py:23: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_version.py::test_unknown_fallback_without_build_or_distribution_version - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 341 passed, 3 deselected, 1 warning in 4.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 0, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -32,7 +32,7 @@
"""
log_format = settings.log_format.lower()
- if log_format == "json":
+ if log_format != "json":
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................F
=================================== FAILURES ===================================
________________________ test_get_log_renderer_console _________________________
def test_get_log_renderer_console():
"""Test console renderer selection."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "console"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f883181f890>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:46: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_console - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f883181f890>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 189 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 1, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -34,7 +34,7 @@
if log_format == "json":
return structlog.processors.JSONRenderer()
- elif log_format == "console":
+ elif log_format != "console":
return structlog.dev.ConsoleRenderer(colors=True)
elif log_format == "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................F
=================================== FAILURES ===================================
________________________ test_get_log_renderer_console _________________________
def test_get_log_renderer_console():
"""Test console renderer selection."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "console"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f672539bd90>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:46: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_console - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f672539bd90>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 189 passed, 3 deselected, 1 warning in 3.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 2, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -36,7 +36,7 @@
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)
- elif log_format == "auto":
+ elif log_format != "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise
if is_running_in_kubernetes():
return structlog.processors.JSONRenderer()........................................................................ [ 15%]
........................................................................ [ 31%]
...............................................F
=================================== FAILURES ===================================
_______________________ test_get_log_renderer_auto_local _______________________
def test_get_log_renderer_auto_local():
"""Test auto renderer in local environment."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "auto"
with patch("stampbot.logger.is_running_in_kubernetes", return_value=False):
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7fc50b7a2650>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:68: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_auto_local - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7fc50b7a2650>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 191 passed, 3 deselected, 1 warning in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 0, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -32,7 +32,7 @@
"""
log_format = settings.log_format.lower()
- if log_format == "json":
+ if log_format < "json":
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................F
=================================== FAILURES ===================================
________________________ test_get_log_renderer_console _________________________
def test_get_log_renderer_console():
"""Test console renderer selection."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "console"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7fcd9de574d0>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:46: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_console - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7fcd9de574d0>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 189 passed, 3 deselected, 1 warning in 2.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 1, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -34,7 +34,7 @@
if log_format == "json":
return structlog.processors.JSONRenderer()
- elif log_format == "console":
+ elif log_format < "console":
return structlog.dev.ConsoleRenderer(colors=True)
elif log_format == "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................F
=================================== FAILURES ===================================
________________________ test_get_log_renderer_console _________________________
def test_get_log_renderer_console():
"""Test console renderer selection."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "console"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f744da82ad0>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:46: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_console - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f744da82ad0>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 189 passed, 3 deselected, 1 warning in 2.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 2, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -36,7 +36,7 @@
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)
- elif log_format == "auto":
+ elif log_format < "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise
if is_running_in_kubernetes():
return structlog.processors.JSONRenderer()........................................................................ [ 15%]
........................................................................ [ 31%]
...............................................F
=================================== FAILURES ===================================
_______________________ test_get_log_renderer_auto_local _______________________
def test_get_log_renderer_auto_local():
"""Test auto renderer in local environment."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "auto"
with patch("stampbot.logger.is_running_in_kubernetes", return_value=False):
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7fe83bb6a650>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:68: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_auto_local - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7fe83bb6a650>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 191 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 0, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -32,7 +32,7 @@
"""
log_format = settings.log_format.lower()
- if log_format == "json":
+ if log_format <= "json":
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................F
=================================== FAILURES ===================================
________________________ test_get_log_renderer_console _________________________
def test_get_log_renderer_console():
"""Test console renderer selection."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "console"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7efcac8134d0>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:46: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_console - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7efcac8134d0>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 189 passed, 3 deselected, 1 warning in 3.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 1, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -34,7 +34,7 @@
if log_format == "json":
return structlog.processors.JSONRenderer()
- elif log_format == "console":
+ elif log_format <= "console":
return structlog.dev.ConsoleRenderer(colors=True)
elif log_format == "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise........................................................................ [ 15%]
........................................................................ [ 31%]
..............................................F
=================================== FAILURES ===================================
____________________ test_get_log_renderer_auto_kubernetes _____________________
def test_get_log_renderer_auto_kubernetes():
"""Test auto renderer in Kubernetes environment."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "auto"
with patch("stampbot.logger.is_running_in_kubernetes", return_value=True):
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.processors.JSONRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7fa5502f6d70>, <class 'structlog.processors.JSONRenderer'>)
E + where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
E + where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
tests/test_logger.py:57: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_auto_kubernetes - AssertionError: assert False
+ where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7fa5502f6d70>, <class 'structlog.processors.JSONRenderer'>)
+ where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
+ where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 190 passed, 3 deselected, 1 warning in 3.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 2, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -36,7 +36,7 @@
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)
- elif log_format == "auto":
+ elif log_format <= "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise
if is_running_in_kubernetes():
return structlog.processors.JSONRenderer()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 0, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -32,7 +32,7 @@
"""
log_format = settings.log_format.lower()
- if log_format == "json":
+ if log_format > "json":
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 1, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -34,7 +34,7 @@
if log_format == "json":
return structlog.processors.JSONRenderer()
- elif log_format == "console":
+ elif log_format > "console":
return structlog.dev.ConsoleRenderer(colors=True)
elif log_format == "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................F
=================================== FAILURES ===================================
________________________ test_get_log_renderer_console _________________________
def test_get_log_renderer_console():
"""Test console renderer selection."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "console"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f48679d7d90>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:46: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_console - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f48679d7d90>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 189 passed, 3 deselected, 1 warning in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 2, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -36,7 +36,7 @@
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)
- elif log_format == "auto":
+ elif log_format > "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise
if is_running_in_kubernetes():
return structlog.processors.JSONRenderer()........................................................................ [ 15%]
........................................................................ [ 31%]
...............................................F
=================================== FAILURES ===================================
_______________________ test_get_log_renderer_auto_local _______________________
def test_get_log_renderer_auto_local():
"""Test auto renderer in local environment."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "auto"
with patch("stampbot.logger.is_running_in_kubernetes", return_value=False):
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f66d9bda650>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:68: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_auto_local - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f66d9bda650>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 191 passed, 3 deselected, 1 warning in 3.03s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 0, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -32,7 +32,7 @@
"""
log_format = settings.log_format.lower()
- if log_format == "json":
+ if log_format >= "json":
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.25s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 1, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -34,7 +34,7 @@
if log_format == "json":
return structlog.processors.JSONRenderer()
- elif log_format == "console":
+ elif log_format >= "console":
return structlog.dev.ConsoleRenderer(colors=True)
elif log_format == "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise........................................................................ [ 15%]
........................................................................ [ 31%]
................................................F
=================================== FAILURES ===================================
_____________________ test_get_log_renderer_unknown_format _____________________
def test_get_log_renderer_unknown_format():
"""Test fallback to JSON for unknown format."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "unknown_format"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.processors.JSONRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7f6be1fd3490>, <class 'structlog.processors.JSONRenderer'>)
E + where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
E + where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
tests/test_logger.py:78: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_unknown_format - AssertionError: assert False
+ where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7f6be1fd3490>, <class 'structlog.processors.JSONRenderer'>)
+ where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
+ where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 192 passed, 3 deselected, 1 warning in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 2, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -36,7 +36,7 @@
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)
- elif log_format == "auto":
+ elif log_format >= "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise
if is_running_in_kubernetes():
return structlog.processors.JSONRenderer()........................................................................ [ 15%]
........................................................................ [ 31%]
................................................F
=================================== FAILURES ===================================
_____________________ test_get_log_renderer_unknown_format _____________________
def test_get_log_renderer_unknown_format():
"""Test fallback to JSON for unknown format."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "unknown_format"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.processors.JSONRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7f0196553490>, <class 'structlog.processors.JSONRenderer'>)
E + where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
E + where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
tests/test_logger.py:78: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_unknown_format - AssertionError: assert False
+ where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7f0196553490>, <class 'structlog.processors.JSONRenderer'>)
+ where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
+ where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 192 passed, 3 deselected, 1 warning in 2.97s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 0, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -32,7 +32,7 @@
"""
log_format = settings.log_format.lower()
- if log_format == "json":
+ if log_format is "json":
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/logger.py:35
/home/runner/work/stampbot/stampbot/stampbot/logger.py:35: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if log_format is "json":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 2 warnings in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 1, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -34,7 +34,7 @@
if log_format == "json":
return structlog.processors.JSONRenderer()
- elif log_format == "console":
+ elif log_format is "console":
return structlog.dev.ConsoleRenderer(colors=True)
elif log_format == "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................F
=================================== FAILURES ===================================
________________________ test_get_log_renderer_console _________________________
def test_get_log_renderer_console():
"""Test console renderer selection."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "console"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7fae5909f890>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:46: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/logger.py:37
/home/runner/work/stampbot/stampbot/stampbot/logger.py:37: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
elif log_format is "console":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_console - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7fae5909f890>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 189 passed, 3 deselected, 2 warnings in 3.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 2, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -36,7 +36,7 @@
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)
- elif log_format == "auto":
+ elif log_format is "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise
if is_running_in_kubernetes():
return structlog.processors.JSONRenderer()........................................................................ [ 15%]
........................................................................ [ 31%]
...............................................F
=================================== FAILURES ===================================
_______________________ test_get_log_renderer_auto_local _______________________
def test_get_log_renderer_auto_local():
"""Test auto renderer in local environment."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "auto"
with patch("stampbot.logger.is_running_in_kubernetes", return_value=False):
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7ff68ddf2650>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:68: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/logger.py:39
/home/runner/work/stampbot/stampbot/stampbot/logger.py:39: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
elif log_format is "auto":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_auto_local - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7ff68ddf2650>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 191 passed, 3 deselected, 2 warnings in 2.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 0, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -32,7 +32,7 @@
"""
log_format = settings.log_format.lower()
- if log_format == "json":
+ if log_format is not "json":
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................F
=================================== FAILURES ===================================
________________________ test_get_log_renderer_console _________________________
def test_get_log_renderer_console():
"""Test console renderer selection."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "console"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f963b7f7890>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:46: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/logger.py:35
/home/runner/work/stampbot/stampbot/stampbot/logger.py:35: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if log_format is not "json":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_console - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f963b7f7890>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 189 passed, 3 deselected, 2 warnings in 3.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 1, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -34,7 +34,7 @@
if log_format == "json":
return structlog.processors.JSONRenderer()
- elif log_format == "console":
+ elif log_format is not "console":
return structlog.dev.ConsoleRenderer(colors=True)
elif log_format == "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise........................................................................ [ 15%]
........................................................................ [ 31%]
..............................................F
=================================== FAILURES ===================================
____________________ test_get_log_renderer_auto_kubernetes _____________________
def test_get_log_renderer_auto_kubernetes():
"""Test auto renderer in Kubernetes environment."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "auto"
with patch("stampbot.logger.is_running_in_kubernetes", return_value=True):
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.processors.JSONRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7f5e4b0ead70>, <class 'structlog.processors.JSONRenderer'>)
E + where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
E + where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
tests/test_logger.py:57: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/logger.py:37
/home/runner/work/stampbot/stampbot/stampbot/logger.py:37: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
elif log_format is not "console":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_auto_kubernetes - AssertionError: assert False
+ where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7f5e4b0ead70>, <class 'structlog.processors.JSONRenderer'>)
+ where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
+ where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 190 passed, 3 deselected, 2 warnings in 2.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 2, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -36,7 +36,7 @@
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)
- elif log_format == "auto":
+ elif log_format is not "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise
if is_running_in_kubernetes():
return structlog.processors.JSONRenderer()........................................................................ [ 15%]
........................................................................ [ 31%]
................................................F
=================================== FAILURES ===================================
_____________________ test_get_log_renderer_unknown_format _____________________
def test_get_log_renderer_unknown_format():
"""Test fallback to JSON for unknown format."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "unknown_format"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.processors.JSONRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7fa2c77fb490>, <class 'structlog.processors.JSONRenderer'>)
E + where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
E + where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
tests/test_logger.py:78: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/logger.py:39
/home/runner/work/stampbot/stampbot/stampbot/logger.py:39: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
elif log_format is not "auto":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_unknown_format - AssertionError: assert False
+ where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7fa2c77fb490>, <class 'structlog.processors.JSONRenderer'>)
+ where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
+ where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 192 passed, 3 deselected, 2 warnings in 2.94s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 0, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -32,7 +32,7 @@
"""
log_format = settings.log_format.lower()
- if log_format == "json":
+ if not log_format == "json":
return structlog.processors.JSONRenderer()
elif log_format == "console":
return structlog.dev.ConsoleRenderer(colors=True)........................................................................ [ 15%]
........................................................................ [ 31%]
.............................................F
=================================== FAILURES ===================================
________________________ test_get_log_renderer_console _________________________
def test_get_log_renderer_console():
"""Test console renderer selection."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "console"
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.dev.ConsoleRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f2ae2b37c50>, <class 'structlog.dev.ConsoleRenderer'>)
E + where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
E + where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
tests/test_logger.py:46: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_console - AssertionError: assert False
+ where False = isinstance(<structlog.processors.JSONRenderer object at 0x7f2ae2b37c50>, <class 'structlog.dev.ConsoleRenderer'>)
+ where <class 'structlog.dev.ConsoleRenderer'> = <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'>.ConsoleRenderer
+ where <module 'structlog.dev' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/dev.py'> = structlog.dev
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 189 passed, 3 deselected, 1 warning in 2.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 1, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -38,7 +38,7 @@
return structlog.dev.ConsoleRenderer(colors=True)
elif log_format == "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise
- if is_running_in_kubernetes():
+ if not is_running_in_kubernetes():
return structlog.processors.JSONRenderer()
else:
return structlog.dev.ConsoleRenderer(colors=True)........................................................................ [ 15%]
........................................................................ [ 31%]
..............................................F
=================================== FAILURES ===================================
____________________ test_get_log_renderer_auto_kubernetes _____________________
def test_get_log_renderer_auto_kubernetes():
"""Test auto renderer in Kubernetes environment."""
with patch("stampbot.logger.settings") as mock_settings:
mock_settings.log_format = "auto"
with patch("stampbot.logger.is_running_in_kubernetes", return_value=True):
from stampbot.logger import _get_log_renderer
renderer = _get_log_renderer()
> assert isinstance(renderer, structlog.processors.JSONRenderer)
E AssertionError: assert False
E + where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7fec3af33d90>, <class 'structlog.processors.JSONRenderer'>)
E + where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
E + where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
tests/test_logger.py:57: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_logger.py::test_get_log_renderer_auto_kubernetes - AssertionError: assert False
+ where False = isinstance(<structlog.dev.ConsoleRenderer object at 0x7fec3af33d90>, <class 'structlog.processors.JSONRenderer'>)
+ where <class 'structlog.processors.JSONRenderer'> = <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'>.JSONRenderer
+ where <module 'structlog.processors' from '/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/structlog/processors.py'> = structlog.processors
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 190 passed, 3 deselected, 1 warning in 2.98s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 0, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -35,7 +35,7 @@
if log_format == "json":
return structlog.processors.JSONRenderer()
elif log_format == "console":
- return structlog.dev.ConsoleRenderer(colors=True)
+ return structlog.dev.ConsoleRenderer(colors=False)
elif log_format == "auto":
# Auto-detect: use JSON in Kubernetes, console otherwise
if is_running_in_kubernetes():........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 1, definition_name: _get_log_renderer
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -41,7 +41,7 @@
if is_running_in_kubernetes():
return structlog.processors.JSONRenderer()
else:
- return structlog.dev.ConsoleRenderer(colors=True)
+ return structlog.dev.ConsoleRenderer(colors=False)
else:
# Unknown format, default to JSON for safety
return structlog.processors.JSONRenderer()........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 2, definition_name: configure_logging
--- mutation diff ---
--- a/stampbot/logger.py
+++ b/stampbot/logger.py
@@ -80,7 +80,7 @@
context_class=dict,
# Route structlog through stdlib so all output shares one handler.
logger_factory=structlog.stdlib.LoggerFactory(),
- cache_logger_on_first_use=True,
+ cache_logger_on_first_use=False,
)
# ProcessorFormatter ensures uvicorn access logs and every other stdlib........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.19s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Add, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS + 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
assert is_eligible is False
> assert f"{int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)} ms safety limit" in reason
E AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 1000 ms safety limit'
tests/test_config.py:450: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 1000 ms safety limit'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Sub, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS - 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
assert is_eligible is False
> assert f"{int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)} ms safety limit" in reason
E AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the -999 ms safety limit'
tests/test_config.py:450: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the -999 ms safety limit'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.81s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Div, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS / 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
assert is_eligible is False
> assert f"{int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)} ms safety limit" in reason
E AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 0 ms safety limit'
tests/test_config.py:450: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 0 ms safety limit'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_FloorDiv, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS // 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
assert is_eligible is False
> assert f"{int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)} ms safety limit" in reason
E AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 0 ms safety limit'
tests/test_config.py:450: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 0 ms safety limit'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Mod, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS % 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
assert is_eligible is False
> assert f"{int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)} ms safety limit" in reason
E AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 0 ms safety limit'
tests/test_config.py:450: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 0 ms safety limit'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_Pow, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS ** 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
assert is_eligible is False
> assert f"{int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)} ms safety limit" in reason
E AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 0 ms safety limit'
tests/test_config.py:450: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 0 ms safety limit'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_RShift, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS >> 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
self = <stampbot.config.RepoConfig object at 0x7f29221e7890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
> if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
E TimeoutError: regex timed out
stampbot/config.py:320: TimeoutError
During handling of the above exception, another exception occurred:
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
> is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
tests/test_config.py:445:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7f29221e7890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
break
except TimeoutError:
> timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS >> 1000)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for >>: 'float' and 'int'
stampbot/config.py:327: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - TypeError: unsupported operand type(s) for >>: 'float' and 'int'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_LShift, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS << 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
self = <stampbot.config.RepoConfig object at 0x7feb56e27890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
> if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
E TimeoutError: regex timed out
stampbot/config.py:320: TimeoutError
During handling of the above exception, another exception occurred:
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
> is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
tests/test_config.py:445:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7feb56e27890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
break
except TimeoutError:
> timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS << 1000)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for <<: 'float' and 'int'
stampbot/config.py:327: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - TypeError: unsupported operand type(s) for <<: 'float' and 'int'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.89s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitOr, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS | 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
self = <stampbot.config.RepoConfig object at 0x7f304bd2b890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
> if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
E TimeoutError: regex timed out
stampbot/config.py:320: TimeoutError
During handling of the above exception, another exception occurred:
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
> is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
tests/test_config.py:445:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7f304bd2b890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
break
except TimeoutError:
> timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS | 1000)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for |: 'float' and 'int'
stampbot/config.py:327: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - TypeError: unsupported operand type(s) for |: 'float' and 'int'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitAnd, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS & 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
self = <stampbot.config.RepoConfig object at 0x7feac86e3890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
> if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
E TimeoutError: regex timed out
stampbot/config.py:320: TimeoutError
During handling of the above exception, another exception occurred:
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
> is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
tests/test_config.py:445:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7feac86e3890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
break
except TimeoutError:
> timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS & 1000)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for &: 'float' and 'int'
stampbot/config.py:327: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - TypeError: unsupported operand type(s) for &: 'float' and 'int'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Mul_BitXor, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS ^ 1000)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
self = <stampbot.config.RepoConfig object at 0x7fd3ab577890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
> if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
E TimeoutError: regex timed out
stampbot/config.py:320: TimeoutError
During handling of the above exception, another exception occurred:
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
> is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
tests/test_config.py:445:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7fd3ab577890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
break
except TimeoutError:
> timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS ^ 1000)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: unsupported operand type(s) for ^: 'float' and 'int'
stampbot/config.py:327: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - TypeError: unsupported operand type(s) for ^: 'float' and 'int'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Add, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {+defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {+defaults, **repo_settings}
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Add, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, +repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, +repo_settings}
E ^
E SyntaxError: ':' expected after dictionary key
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Sub, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {-defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {-defaults, **repo_settings}
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Sub, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, -repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, -repo_settings}
E ^
E SyntaxError: ':' expected after dictionary key
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Mul, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {*defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {*defaults, **repo_settings}
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Mul, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, *repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, *repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Div, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {/defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {/defaults, **repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Div, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, /repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, /repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_FloorDiv, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {//defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {//defaults, **repo_settings}
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_FloorDiv, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, //repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, //repo_settings}
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Mod, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {%defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {%defaults, **repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_Mod, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, %repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, %repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_RShift, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {>>defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {>>defaults, **repo_settings}
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_RShift, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, >>repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, >>repo_settings}
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_LShift, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {<<defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {<<defaults, **repo_settings}
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_LShift, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, <<repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, <<repo_settings}
E ^^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_BitOr, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {|defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {|defaults, **repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_BitOr, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, |repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, |repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_BitAnd, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {&defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {&defaults, **repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_BitAnd, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, &repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, &repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_BitXor, occurrence: 0, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {^defaults, **repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {^defaults, **repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBinaryOperator_Pow_BitXor, occurrence: 1, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -422,7 +422,7 @@
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults
- merged = {**defaults, **repo_settings}
+ merged = {**defaults, ^repo_settings}
title_patterns = merged.get("required_title_patterns", [])
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_config.py _____________________
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/python.py:508: in importtestmodule
mod = import_path(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/pathlib.py:596: in import_path
importlib.import_module(module_name)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/importlib/__init__.py:88: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<frozen importlib._bootstrap>:1406: in _gcd_import
???
<frozen importlib._bootstrap>:1371: in _find_and_load
???
<frozen importlib._bootstrap>:1342: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:938: in _load_unlocked
???
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/_pytest/assertion/rewrite.py:188: in exec_module
exec(co, module.__dict__)
tests/test_config.py:8: in <module>
from stampbot.config import (
E File "/home/runner/work/stampbot/stampbot/stampbot/config.py", line 425
E merged = {**defaults, ^repo_settings}
E ^
E SyntaxError: invalid syntax
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_config.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_Eq, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -90,7 +90,7 @@
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
- if seconds < 0:
+ if seconds == 0:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
return seconds
............................................................F
=================================== FAILURES ===================================
_____________ test_repo_config_cache_seconds_accepts_integers[0-0] _____________
raw = 0, expected = 0
@pytest.mark.parametrize(
("raw", "expected"),
[(300, 300), ("120", 120), (0, 0), (5.0, 5)],
)
def test_repo_config_cache_seconds_accepts_integers(raw, expected):
"""Test the cache lifetime accepts integer-valued settings."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = raw
> assert repo_config_cache_seconds() == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:627:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def repo_config_cache_seconds() -> int:
"""Return how long a repository's parsed policy stays cached, in seconds.
Returns:
A non-negative integer. Zero disables the cache.
Raises:
ValueError: If ``repo_config_cache_seconds`` is not a non-negative integer.
"""
raw = settings.get("repo_config_cache_seconds", DEFAULT_REPO_CONFIG_CACHE_SECONDS)
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
if seconds == 0:
> raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
E ValueError: repo_config_cache_seconds must be 0 or greater, got 0
stampbot/config.py:94: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_accepts_integers[0-0] - ValueError: repo_config_cache_seconds must be 0 or greater, got 0
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 60 passed, 3 deselected, 1 warning in 2.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_NotEq, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -90,7 +90,7 @@
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
- if seconds < 0:
+ if seconds != 0:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
return seconds
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
stampbot/webhook_handler.py:1149: in <module>
webhook_handler = WebhookHandler()
^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:84: in __init__
self._policy = RepoPolicyResolver(repo_config_cache_seconds())
^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/config.py:94: in repo_config_cache_seconds
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
E ValueError: repo_config_cache_seconds must be 0 or greater, got 300
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: repo_config_cache_seconds must be 0 or greater, got 300
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.28s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_LtE, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -90,7 +90,7 @@
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
- if seconds < 0:
+ if seconds <= 0:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
return seconds
............................................................F
=================================== FAILURES ===================================
_____________ test_repo_config_cache_seconds_accepts_integers[0-0] _____________
raw = 0, expected = 0
@pytest.mark.parametrize(
("raw", "expected"),
[(300, 300), ("120", 120), (0, 0), (5.0, 5)],
)
def test_repo_config_cache_seconds_accepts_integers(raw, expected):
"""Test the cache lifetime accepts integer-valued settings."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = raw
> assert repo_config_cache_seconds() == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:627:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def repo_config_cache_seconds() -> int:
"""Return how long a repository's parsed policy stays cached, in seconds.
Returns:
A non-negative integer. Zero disables the cache.
Raises:
ValueError: If ``repo_config_cache_seconds`` is not a non-negative integer.
"""
raw = settings.get("repo_config_cache_seconds", DEFAULT_REPO_CONFIG_CACHE_SECONDS)
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
if seconds <= 0:
> raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
E ValueError: repo_config_cache_seconds must be 0 or greater, got 0
stampbot/config.py:94: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_accepts_integers[0-0] - ValueError: repo_config_cache_seconds must be 0 or greater, got 0
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 60 passed, 3 deselected, 1 warning in 2.72s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_Gt, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -90,7 +90,7 @@
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
- if seconds < 0:
+ if seconds > 0:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
return seconds
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
stampbot/webhook_handler.py:1149: in <module>
webhook_handler = WebhookHandler()
^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:84: in __init__
self._policy = RepoPolicyResolver(repo_config_cache_seconds())
^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/config.py:94: in repo_config_cache_seconds
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
E ValueError: repo_config_cache_seconds must be 0 or greater, got 300
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: repo_config_cache_seconds must be 0 or greater, got 300
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_GtE, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -90,7 +90,7 @@
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
- if seconds < 0:
+ if seconds >= 0:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
return seconds
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
stampbot/webhook_handler.py:1149: in <module>
webhook_handler = WebhookHandler()
^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:84: in __init__
self._policy = RepoPolicyResolver(repo_config_cache_seconds())
^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/config.py:94: in repo_config_cache_seconds
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
E ValueError: repo_config_cache_seconds must be 0 or greater, got 300
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: repo_config_cache_seconds must be 0 or greater, got 300
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 0, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -225,7 +225,7 @@
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
- if len(patterns) > MAX_TITLE_PATTERN_COUNT:
+ if len(patterns) == MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})".............................F
=================================== FAILURES ===================================
_______________ test_repo_config_rejects_too_many_title_patterns _______________
def test_repo_config_rejects_too_many_title_patterns():
"""Test title pattern count is bounded."""
patterns = ", ".join('"^fix:"' for _ in range(MAX_TITLE_PATTERN_COUNT + 1))
> with pytest.raises(ValueError, match="too many patterns"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_config.py:327: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_rejects_too_many_title_patterns - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 29 passed, 3 deselected, 1 warning in 2.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 1, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -235,7 +235,7 @@
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
- if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
+ if len(pattern) == MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"..............................F
=================================== FAILURES ===================================
_________________ test_repo_config_rejects_long_title_pattern __________________
def test_repo_config_rejects_long_title_pattern():
"""Test individual title pattern length is bounded."""
pattern = "a" * (MAX_TITLE_PATTERN_LENGTH + 1)
> with pytest.raises(ValueError, match="pattern longer than"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_config.py:335: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_rejects_long_title_pattern - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 30 passed, 3 deselected, 1 warning in 2.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Eq, occurrence: 2, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -309,7 +309,7 @@
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
- if len(pr_title) > MAX_PR_TITLE_LENGTH:
+ if len(pr_title) == MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",........................................F
=================================== FAILURES ===================================
__________ test_is_pr_eligible_rejects_overlong_title_before_matching __________
def test_is_pr_eligible_rejects_overlong_title_before_matching():
"""Test webhook title input is bounded before regex evaluation."""
config = RepoConfig.from_toml('required_title_patterns = [".*"]')
config._compiled_title_patterns[0] = MagicMock()
is_eligible, reason = config.is_pr_eligible([], "a" * (MAX_PR_TITLE_LENGTH + 1), "someuser")
> assert is_eligible is False
E assert True is False
tests/test_config.py:426: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_rejects_overlong_title_before_matching - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 40 passed, 3 deselected, 1 warning in 2.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 0, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -225,7 +225,7 @@
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
- if len(patterns) > MAX_TITLE_PATTERN_COUNT:
+ if len(patterns) != MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = []
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) != MAX_TITLE_PATTERN_COUNT:
> raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
E ValueError: required_title_patterns contains too many patterns (maximum: 20)
stampbot/config.py:229: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: required_title_patterns contains too many patterns (maximum: 20)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 1, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -235,7 +235,7 @@
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
- if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
+ if len(pattern) != MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters".........................F
=================================== FAILURES ===================================
________________ test_repo_config_with_required_title_patterns _________________
def test_repo_config_with_required_title_patterns():
"""Test parsing repository config with required title patterns."""
toml_content = """
required_title_patterns = ["^\\\\[bot\\\\]", "^chore:"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:295:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['^\\[bot\\]', '^chore:']
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) != MAX_TITLE_PATTERN_LENGTH:
> raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"
)
E ValueError: required_title_patterns contains a pattern longer than 256 characters
stampbot/config.py:239: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_with_required_title_patterns - ValueError: required_title_patterns contains a pattern longer than 256 characters
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 25 passed, 3 deselected, 1 warning in 2.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_NotEq, occurrence: 2, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -309,7 +309,7 @@
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
- if len(pr_title) > MAX_PR_TITLE_LENGTH:
+ if len(pr_title) != MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_matches ___________________
def test_is_pr_eligible_title_pattern_matches():
"""Test is_pr_eligible returns True when title matches pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "chore: update dependencies", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:386: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_matches - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 36 passed, 3 deselected, 1 warning in 2.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 0, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -225,7 +225,7 @@
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
- if len(patterns) > MAX_TITLE_PATTERN_COUNT:
+ if len(patterns) < MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = []
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) < MAX_TITLE_PATTERN_COUNT:
> raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
E ValueError: required_title_patterns contains too many patterns (maximum: 20)
stampbot/config.py:229: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: required_title_patterns contains too many patterns (maximum: 20)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 1, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -235,7 +235,7 @@
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
- if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
+ if len(pattern) < MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters".........................F
=================================== FAILURES ===================================
________________ test_repo_config_with_required_title_patterns _________________
def test_repo_config_with_required_title_patterns():
"""Test parsing repository config with required title patterns."""
toml_content = """
required_title_patterns = ["^\\\\[bot\\\\]", "^chore:"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:295:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['^\\[bot\\]', '^chore:']
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) < MAX_TITLE_PATTERN_LENGTH:
> raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"
)
E ValueError: required_title_patterns contains a pattern longer than 256 characters
stampbot/config.py:239: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_with_required_title_patterns - ValueError: required_title_patterns contains a pattern longer than 256 characters
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 25 passed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Lt, occurrence: 2, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -309,7 +309,7 @@
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
- if len(pr_title) > MAX_PR_TITLE_LENGTH:
+ if len(pr_title) < MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_matches ___________________
def test_is_pr_eligible_title_pattern_matches():
"""Test is_pr_eligible returns True when title matches pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "chore: update dependencies", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:386: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_matches - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 36 passed, 3 deselected, 1 warning in 2.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 0, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -225,7 +225,7 @@
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
- if len(patterns) > MAX_TITLE_PATTERN_COUNT:
+ if len(patterns) <= MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = []
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) <= MAX_TITLE_PATTERN_COUNT:
> raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
E ValueError: required_title_patterns contains too many patterns (maximum: 20)
stampbot/config.py:229: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: required_title_patterns contains too many patterns (maximum: 20)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 1, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -235,7 +235,7 @@
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
- if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
+ if len(pattern) <= MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters".........................F
=================================== FAILURES ===================================
________________ test_repo_config_with_required_title_patterns _________________
def test_repo_config_with_required_title_patterns():
"""Test parsing repository config with required title patterns."""
toml_content = """
required_title_patterns = ["^\\\\[bot\\\\]", "^chore:"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:295:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['^\\[bot\\]', '^chore:']
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) <= MAX_TITLE_PATTERN_LENGTH:
> raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"
)
E ValueError: required_title_patterns contains a pattern longer than 256 characters
stampbot/config.py:239: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_with_required_title_patterns - ValueError: required_title_patterns contains a pattern longer than 256 characters
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 25 passed, 3 deselected, 1 warning in 2.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_LtE, occurrence: 2, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -309,7 +309,7 @@
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
- if len(pr_title) > MAX_PR_TITLE_LENGTH:
+ if len(pr_title) <= MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_matches ___________________
def test_is_pr_eligible_title_pattern_matches():
"""Test is_pr_eligible returns True when title matches pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "chore: update dependencies", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:386: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_matches - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 36 passed, 3 deselected, 1 warning in 2.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 0, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -225,7 +225,7 @@
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
- if len(patterns) > MAX_TITLE_PATTERN_COUNT:
+ if len(patterns) >= MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"...............................F
=================================== FAILURES ===================================
______________ test_repo_config_accepts_title_pattern_boundaries _______________
def test_repo_config_accepts_title_pattern_boundaries():
"""Test pattern count and length limits are inclusive."""
patterns = ["a" * MAX_TITLE_PATTERN_LENGTH]
patterns.extend("^fix:" for _ in range(MAX_TITLE_PATTERN_COUNT - 1))
> config = RepoConfig.from_toml(f"required_title_patterns = {patterns!r}")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:344:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '^fix:', '^fix:', '^fix:', '^fix:', '^fix:', ...]
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) >= MAX_TITLE_PATTERN_COUNT:
> raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
E ValueError: required_title_patterns contains too many patterns (maximum: 20)
stampbot/config.py:229: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_accepts_title_pattern_boundaries - ValueError: required_title_patterns contains too many patterns (maximum: 20)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 31 passed, 3 deselected, 1 warning in 2.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 1, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -235,7 +235,7 @@
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
- if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
+ if len(pattern) >= MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"...............................F
=================================== FAILURES ===================================
______________ test_repo_config_accepts_title_pattern_boundaries _______________
def test_repo_config_accepts_title_pattern_boundaries():
"""Test pattern count and length limits are inclusive."""
patterns = ["a" * MAX_TITLE_PATTERN_LENGTH]
patterns.extend("^fix:" for _ in range(MAX_TITLE_PATTERN_COUNT - 1))
> config = RepoConfig.from_toml(f"required_title_patterns = {patterns!r}")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:344:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '^fix:', '^fix:', '^fix:', '^fix:', '^fix:', ...]
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) >= MAX_TITLE_PATTERN_LENGTH:
> raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"
)
E ValueError: required_title_patterns contains a pattern longer than 256 characters
stampbot/config.py:239: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_accepts_title_pattern_boundaries - ValueError: required_title_patterns contains a pattern longer than 256 characters
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 31 passed, 3 deselected, 1 warning in 2.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_GtE, occurrence: 2, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -309,7 +309,7 @@
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
- if len(pr_title) > MAX_PR_TITLE_LENGTH:
+ if len(pr_title) >= MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
assert is_eligible is False
> assert f"{int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)} ms safety limit" in reason
E AssertionError: assert '10 ms safety limit' in 'PR title exceeds the 256-character safety limit'
tests/test_config.py:450: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - AssertionError: assert '10 ms safety limit' in 'PR title exceeds the 256-character safety limit'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 0, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -225,7 +225,7 @@
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
- if len(patterns) > MAX_TITLE_PATTERN_COUNT:
+ if len(patterns) is MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})".............................F
=================================== FAILURES ===================================
_______________ test_repo_config_rejects_too_many_title_patterns _______________
def test_repo_config_rejects_too_many_title_patterns():
"""Test title pattern count is bounded."""
patterns = ", ".join('"^fix:"' for _ in range(MAX_TITLE_PATTERN_COUNT + 1))
> with pytest.raises(ValueError, match="too many patterns"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_config.py:327: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_rejects_too_many_title_patterns - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 29 passed, 3 deselected, 1 warning in 2.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 1, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -235,7 +235,7 @@
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
- if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
+ if len(pattern) is MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"..............................F
=================================== FAILURES ===================================
_________________ test_repo_config_rejects_long_title_pattern __________________
def test_repo_config_rejects_long_title_pattern():
"""Test individual title pattern length is bounded."""
pattern = "a" * (MAX_TITLE_PATTERN_LENGTH + 1)
> with pytest.raises(ValueError, match="pattern longer than"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_config.py:335: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_rejects_long_title_pattern - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 30 passed, 3 deselected, 1 warning in 2.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_Is, occurrence: 2, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -309,7 +309,7 @@
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
- if len(pr_title) > MAX_PR_TITLE_LENGTH:
+ if len(pr_title) is MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",........................................F
=================================== FAILURES ===================================
__________ test_is_pr_eligible_rejects_overlong_title_before_matching __________
def test_is_pr_eligible_rejects_overlong_title_before_matching():
"""Test webhook title input is bounded before regex evaluation."""
config = RepoConfig.from_toml('required_title_patterns = [".*"]')
config._compiled_title_patterns[0] = MagicMock()
is_eligible, reason = config.is_pr_eligible([], "a" * (MAX_PR_TITLE_LENGTH + 1), "someuser")
> assert is_eligible is False
E assert True is False
tests/test_config.py:426: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_rejects_overlong_title_before_matching - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 40 passed, 3 deselected, 1 warning in 2.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 0, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -225,7 +225,7 @@
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
- if len(patterns) > MAX_TITLE_PATTERN_COUNT:
+ if len(patterns) is not MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = []
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) is not MAX_TITLE_PATTERN_COUNT:
> raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
E ValueError: required_title_patterns contains too many patterns (maximum: 20)
stampbot/config.py:229: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: required_title_patterns contains too many patterns (maximum: 20)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 1, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -235,7 +235,7 @@
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
- if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
+ if len(pattern) is not MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters".........................F
=================================== FAILURES ===================================
________________ test_repo_config_with_required_title_patterns _________________
def test_repo_config_with_required_title_patterns():
"""Test parsing repository config with required title patterns."""
toml_content = """
required_title_patterns = ["^\\\\[bot\\\\]", "^chore:"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:295:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['^\\[bot\\]', '^chore:']
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) is not MAX_TITLE_PATTERN_LENGTH:
> raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"
)
E ValueError: required_title_patterns contains a pattern longer than 256 characters
stampbot/config.py:239: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_with_required_title_patterns - ValueError: required_title_patterns contains a pattern longer than 256 characters
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 25 passed, 3 deselected, 1 warning in 2.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Gt_IsNot, occurrence: 2, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -309,7 +309,7 @@
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
- if len(pr_title) > MAX_PR_TITLE_LENGTH:
+ if len(pr_title) is not MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_matches ___________________
def test_is_pr_eligible_title_pattern_matches():
"""Test is_pr_eligible returns True when title matches pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "chore: update dependencies", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:386: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_matches - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 36 passed, 3 deselected, 1 warning in 2.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 0, definition_name: __init__
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -162,7 +162,7 @@
"unapprove_commands",
)
self.required_labels = required_labels or []
- title_patterns = required_title_patterns if required_title_patterns is not None else []
+ title_patterns = required_title_patterns if required_title_patterns is None else []
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
self.required_title_patterns = list(title_patterns)
self.allowed_users = allowed_users or []....................F
=================================== FAILURES ===================================
________________ test_repo_config_detaches_valid_command_lists _________________
def test_repo_config_detaches_valid_command_lists():
"""Test a caller cannot mutate command settings through its input lists."""
approve_commands = ["approve"]
unapprove_commands = ["unapprove"]
> config = RepoConfig(
approval_labels=[],
auto_approve_on_label=True,
reapprove=False,
chatops_enabled=True,
chatops_required_permission="maintain",
approve_commands=approve_commands,
unapprove_commands=unapprove_commands,
)
tests/test_config.py:205:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = None
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
> raise ValueError("required_title_patterns must be a list of strings")
E ValueError: required_title_patterns must be a list of strings
stampbot/config.py:227: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_detaches_valid_command_lists - ValueError: required_title_patterns must be a list of strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 20 passed, 3 deselected, 1 warning in 2.64s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceUnaryOperator_USub_UAdd, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -345,7 +345,7 @@
if self.allowed_teams and author_team_slugs:
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
- team_slug = team.split("/")[-1] if "/" in team else team
+ team_slug = team.split("/")[+1] if "/" in team else team
if team_slug in author_team_slugs:
return True, None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Invert, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -345,7 +345,7 @@
if self.allowed_teams and author_team_slugs:
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
- team_slug = team.split("/")[-1] if "/" in team else team
+ team_slug = team.split("/")[~1] if "/" in team else team
if team_slug in author_team_slugs:
return True, None
.................................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_team _______________________
def test_is_pr_eligible_allowed_team():
"""Test is_pr_eligible returns True when author is in an allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "team-member", author_team_slugs=["release-team"]
)
> assert is_eligible is True
E assert False is True
tests/test_config.py:523: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_team - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 49 passed, 3 deselected, 1 warning in 2.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_USub_Not, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -345,7 +345,7 @@
if self.allowed_teams and author_team_slugs:
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
- team_slug = team.split("/")[-1] if "/" in team else team
+ team_slug = team.split("/")[not 1] if "/" in team else team
if team_slug in author_team_slugs:
return True, None
.................................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_team _______________________
def test_is_pr_eligible_allowed_team():
"""Test is_pr_eligible returns True when author is in an allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "team-member", author_team_slugs=["release-team"]
)
> assert is_eligible is True
E assert False is True
tests/test_config.py:523: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_team - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 49 passed, 3 deselected, 1 warning in 2.68s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceUnaryOperator_Delete_USub, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -345,7 +345,7 @@
if self.allowed_teams and author_team_slugs:
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
- team_slug = team.split("/")[-1] if "/" in team else team
+ team_slug = team.split("/")[1] if "/" in team else team
if team_slug in author_team_slugs:
return True, None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -84,7 +84,7 @@
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
- if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
+ if isinstance(raw, bool) or (isinstance(raw, float) and raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw).............................................................F
=================================== FAILURES ===================================
____________ test_repo_config_cache_seconds_accepts_integers[5.0-5] ____________
raw = 5.0, expected = 5
@pytest.mark.parametrize(
("raw", "expected"),
[(300, 300), ("120", 120), (0, 0), (5.0, 5)],
)
def test_repo_config_cache_seconds_accepts_integers(raw, expected):
"""Test the cache lifetime accepts integer-valued settings."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = raw
> assert repo_config_cache_seconds() == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:627:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def repo_config_cache_seconds() -> int:
"""Return how long a repository's parsed policy stays cached, in seconds.
Returns:
A non-negative integer. Zero disables the cache.
Raises:
ValueError: If ``repo_config_cache_seconds`` is not a non-negative integer.
"""
raw = settings.get("repo_config_cache_seconds", DEFAULT_REPO_CONFIG_CACHE_SECONDS)
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
if isinstance(raw, bool) or (isinstance(raw, float) and raw.is_integer()):
> raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
E ValueError: repo_config_cache_seconds must be an integer, got 5.0
stampbot/config.py:88: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_accepts_integers[5.0-5] - ValueError: repo_config_cache_seconds must be an integer, got 5.0
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 61 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 1, definition_name: _validate_command_list
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -183,7 +183,7 @@
Raises:
ValueError: If the setting is not a list of strings.
"""
- if not isinstance(commands, list):
+ if isinstance(commands, list):
raise ValueError(f"{setting} must be a list of strings")
if any(not isinstance(command, str) for command in commands):
raise ValueError(f"{setting} must contain only strings")F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:156: in __init__
self.approve_commands = self._validate_command_list(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
commands = ['approve', 'stamp'], setting = 'approve_commands'
@staticmethod
def _validate_command_list(commands: object, setting: str) -> list[str]:
"""Require one command setting to be a list containing only strings.
Args:
commands: Parsed application or repository setting.
setting: Setting name used in the validation error.
Returns:
A detached copy of the validated command list.
Raises:
ValueError: If the setting is not a list of strings.
"""
if isinstance(commands, list):
> raise ValueError(f"{setting} must be a list of strings")
E ValueError: approve_commands must be a list of strings
stampbot/config.py:187: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: approve_commands must be a list of strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 2, definition_name: _validate_command_list
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -185,7 +185,7 @@
"""
if not isinstance(commands, list):
raise ValueError(f"{setting} must be a list of strings")
- if any(not isinstance(command, str) for command in commands):
+ if any( isinstance(command, str) for command in commands):
raise ValueError(f"{setting} must contain only strings")
return list(commands)
F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:156: in __init__
self.approve_commands = self._validate_command_list(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
commands = ['approve', 'stamp'], setting = 'approve_commands'
@staticmethod
def _validate_command_list(commands: object, setting: str) -> list[str]:
"""Require one command setting to be a list containing only strings.
Args:
commands: Parsed application or repository setting.
setting: Setting name used in the validation error.
Returns:
A detached copy of the validated command list.
Raises:
ValueError: If the setting is not a list of strings.
"""
if not isinstance(commands, list):
raise ValueError(f"{setting} must be a list of strings")
if any( isinstance(command, str) for command in commands):
> raise ValueError(f"{setting} must contain only strings")
E ValueError: approve_commands must contain only strings
stampbot/config.py:189: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: approve_commands must contain only strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 3, definition_name: _validate_required_permission
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -202,7 +202,7 @@
Raises:
ValueError: If the permission is not one of GitHub's repository levels.
"""
- if not isinstance(permission, str) or permission not in REPO_PERMISSION_LEVELS:
+ if isinstance(permission, str) or permission not in REPO_PERMISSION_LEVELS:
raise ValueError(
"Invalid chatops_required_permission: "
f"{permission}. "F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:153: in __init__
self.chatops_required_permission = self._validate_required_permission(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
permission = 'write'
@staticmethod
def _validate_required_permission(permission: object) -> str:
"""Require the ChatOps permission threshold to be a supported value.
Args:
permission: Parsed application or repository setting.
Returns:
The validated permission name.
Raises:
ValueError: If the permission is not one of GitHub's repository levels.
"""
if isinstance(permission, str) or permission not in REPO_PERMISSION_LEVELS:
> raise ValueError(
"Invalid chatops_required_permission: "
f"{permission}. "
f"Valid values: {', '.join(REPO_PERMISSION_LEVELS)}"
)
E ValueError: Invalid chatops_required_permission: write. Valid values: none, read, triage, write, maintain, admin
stampbot/config.py:206: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: Invalid chatops_required_permission: write. Valid values: none, read, triage, write, maintain, admin
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.72s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 4, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -223,7 +223,7 @@
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
- if not isinstance(patterns, list):
+ if isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = []
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if isinstance(patterns, list):
> raise ValueError("required_title_patterns must be a list of strings")
E ValueError: required_title_patterns must be a list of strings
stampbot/config.py:227: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: required_title_patterns must be a list of strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 5, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -233,7 +233,7 @@
compiled_patterns = []
for pattern in patterns:
- if not isinstance(pattern, str):
+ if isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
raise ValueError(.........................F
=================================== FAILURES ===================================
________________ test_repo_config_with_required_title_patterns _________________
def test_repo_config_with_required_title_patterns():
"""Test parsing repository config with required title patterns."""
toml_content = """
required_title_patterns = ["^\\\\[bot\\\\]", "^chore:"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:295:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['^\\[bot\\]', '^chore:']
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if isinstance(pattern, str):
> raise ValueError("required_title_patterns must contain only strings")
E ValueError: required_title_patterns must contain only strings
stampbot/config.py:237: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_with_required_title_patterns - ValueError: required_title_patterns must contain only strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 25 passed, 3 deselected, 1 warning in 2.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 6, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -299,7 +299,7 @@
"""
# Check required labels filter
if self.required_labels:
- if not any(label in self.required_labels for label in pr_labels):
+ if any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",.................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_required_label_present __________________
def test_is_pr_eligible_required_label_present():
"""Test is_pr_eligible returns True when required label is present."""
config = RepoConfig.from_toml('required_labels = ["dependencies", "automated"]')
is_eligible, reason = config.is_pr_eligible(["dependencies"], "PR title", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:361: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_required_label_present - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 33 passed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 7, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -307,7 +307,7 @@
# Check required title patterns filter
if self._compiled_title_patterns:
- if not isinstance(pr_title, str):
+ if isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_matches ___________________
def test_is_pr_eligible_title_pattern_matches():
"""Test is_pr_eligible returns True when title matches pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "chore: update dependencies", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:386: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_matches - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 36 passed, 3 deselected, 1 warning in 2.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 8, definition_name: needs_team_check
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -368,7 +368,7 @@
Returns:
True if team check is needed (author not in allowed_users but teams configured)
"""
- if not self.allowed_teams:
+ if self.allowed_teams:
return False
if self.allowed_users and pr_author in self.allowed_users:
return False.......................................................F
=================================== FAILURES ===================================
__________________ test_needs_team_check_no_teams_configured ___________________
def test_needs_team_check_no_teams_configured():
"""Test needs_team_check returns False when no teams configured."""
config = RepoConfig.from_toml('allowed_users = ["some-user"]')
> assert config.needs_team_check("any-user") is False
E AssertionError: assert True is False
E + where True = needs_team_check('any-user')
E + where needs_team_check = <stampbot.config.RepoConfig object at 0x7f9ddb35f710>.needs_team_check
tests/test_config.py:595: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_needs_team_check_no_teams_configured - AssertionError: assert True is False
+ where True = needs_team_check('any-user')
+ where needs_team_check = <stampbot.config.RepoConfig object at 0x7f9ddb35f710>.needs_team_check
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 55 passed, 3 deselected, 1 warning in 2.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -84,7 +84,7 @@
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
- if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
+ if not isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
stampbot/webhook_handler.py:1149: in <module>
webhook_handler = WebhookHandler()
^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:84: in __init__
self._policy = RepoPolicyResolver(repo_config_cache_seconds())
^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/config.py:88: in repo_config_cache_seconds
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
E ValueError: repo_config_cache_seconds must be an integer, got 300
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: repo_config_cache_seconds must be an integer, got 300
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.25s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 1, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -90,7 +90,7 @@
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
- if seconds < 0:
+ if not seconds < 0:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
return seconds
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:48: in <module>
from stampbot.webhook_handler import webhook_handler
stampbot/webhook_handler.py:1149: in <module>
webhook_handler = WebhookHandler()
^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:84: in __init__
self._policy = RepoPolicyResolver(repo_config_cache_seconds())
^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/config.py:94: in repo_config_cache_seconds
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
E ValueError: repo_config_cache_seconds must be 0 or greater, got 300
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: repo_config_cache_seconds must be 0 or greater, got 300
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.22s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 2, definition_name: __init__
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -162,7 +162,7 @@
"unapprove_commands",
)
self.required_labels = required_labels or []
- title_patterns = required_title_patterns if required_title_patterns is not None else []
+ title_patterns = required_title_patterns if not required_title_patterns is not None else []
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
self.required_title_patterns = list(title_patterns)
self.allowed_users = allowed_users or []....................F
=================================== FAILURES ===================================
________________ test_repo_config_detaches_valid_command_lists _________________
def test_repo_config_detaches_valid_command_lists():
"""Test a caller cannot mutate command settings through its input lists."""
approve_commands = ["approve"]
unapprove_commands = ["unapprove"]
> config = RepoConfig(
approval_labels=[],
auto_approve_on_label=True,
reapprove=False,
chatops_enabled=True,
chatops_required_permission="maintain",
approve_commands=approve_commands,
unapprove_commands=unapprove_commands,
)
tests/test_config.py:205:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = None
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
> raise ValueError("required_title_patterns must be a list of strings")
E ValueError: required_title_patterns must be a list of strings
stampbot/config.py:227: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_detaches_valid_command_lists - ValueError: required_title_patterns must be a list of strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 20 passed, 3 deselected, 1 warning in 2.73s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 3, definition_name: _validate_command_list
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -183,7 +183,7 @@
Raises:
ValueError: If the setting is not a list of strings.
"""
- if not isinstance(commands, list):
+ if not not isinstance(commands, list):
raise ValueError(f"{setting} must be a list of strings")
if any(not isinstance(command, str) for command in commands):
raise ValueError(f"{setting} must contain only strings")F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:156: in __init__
self.approve_commands = self._validate_command_list(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
commands = ['approve', 'stamp'], setting = 'approve_commands'
@staticmethod
def _validate_command_list(commands: object, setting: str) -> list[str]:
"""Require one command setting to be a list containing only strings.
Args:
commands: Parsed application or repository setting.
setting: Setting name used in the validation error.
Returns:
A detached copy of the validated command list.
Raises:
ValueError: If the setting is not a list of strings.
"""
if not not isinstance(commands, list):
> raise ValueError(f"{setting} must be a list of strings")
E ValueError: approve_commands must be a list of strings
stampbot/config.py:187: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: approve_commands must be a list of strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 4, definition_name: _validate_command_list
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -185,7 +185,7 @@
"""
if not isinstance(commands, list):
raise ValueError(f"{setting} must be a list of strings")
- if any(not isinstance(command, str) for command in commands):
+ if not any(not isinstance(command, str) for command in commands):
raise ValueError(f"{setting} must contain only strings")
return list(commands)
F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:156: in __init__
self.approve_commands = self._validate_command_list(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
commands = ['approve', 'stamp'], setting = 'approve_commands'
@staticmethod
def _validate_command_list(commands: object, setting: str) -> list[str]:
"""Require one command setting to be a list containing only strings.
Args:
commands: Parsed application or repository setting.
setting: Setting name used in the validation error.
Returns:
A detached copy of the validated command list.
Raises:
ValueError: If the setting is not a list of strings.
"""
if not isinstance(commands, list):
raise ValueError(f"{setting} must be a list of strings")
if not any(not isinstance(command, str) for command in commands):
> raise ValueError(f"{setting} must contain only strings")
E ValueError: approve_commands must contain only strings
stampbot/config.py:189: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: approve_commands must contain only strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 5, definition_name: _validate_required_permission
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -202,7 +202,7 @@
Raises:
ValueError: If the permission is not one of GitHub's repository levels.
"""
- if not isinstance(permission, str) or permission not in REPO_PERMISSION_LEVELS:
+ if not not isinstance(permission, str) or permission not in REPO_PERMISSION_LEVELS:
raise ValueError(
"Invalid chatops_required_permission: "
f"{permission}. "F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:153: in __init__
self.chatops_required_permission = self._validate_required_permission(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
permission = 'write'
@staticmethod
def _validate_required_permission(permission: object) -> str:
"""Require the ChatOps permission threshold to be a supported value.
Args:
permission: Parsed application or repository setting.
Returns:
The validated permission name.
Raises:
ValueError: If the permission is not one of GitHub's repository levels.
"""
if not not isinstance(permission, str) or permission not in REPO_PERMISSION_LEVELS:
> raise ValueError(
"Invalid chatops_required_permission: "
f"{permission}. "
f"Valid values: {', '.join(REPO_PERMISSION_LEVELS)}"
)
E ValueError: Invalid chatops_required_permission: write. Valid values: none, read, triage, write, maintain, admin
stampbot/config.py:206: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: Invalid chatops_required_permission: write. Valid values: none, read, triage, write, maintain, admin
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 6, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -223,7 +223,7 @@
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
- if not isinstance(patterns, list):
+ if not not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = []
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not not isinstance(patterns, list):
> raise ValueError("required_title_patterns must be a list of strings")
E ValueError: required_title_patterns must be a list of strings
stampbot/config.py:227: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: required_title_patterns must be a list of strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 7, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -225,7 +225,7 @@
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
- if len(patterns) > MAX_TITLE_PATTERN_COUNT:
+ if not len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = []
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if not len(patterns) > MAX_TITLE_PATTERN_COUNT:
> raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
E ValueError: required_title_patterns contains too many patterns (maximum: 20)
stampbot/config.py:229: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - ValueError: required_title_patterns contains too many patterns (maximum: 20)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 8, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -233,7 +233,7 @@
compiled_patterns = []
for pattern in patterns:
- if not isinstance(pattern, str):
+ if not not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
raise ValueError(.........................F
=================================== FAILURES ===================================
________________ test_repo_config_with_required_title_patterns _________________
def test_repo_config_with_required_title_patterns():
"""Test parsing repository config with required title patterns."""
toml_content = """
required_title_patterns = ["^\\\\[bot\\\\]", "^chore:"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:295:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['^\\[bot\\]', '^chore:']
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if not not isinstance(pattern, str):
> raise ValueError("required_title_patterns must contain only strings")
E ValueError: required_title_patterns must contain only strings
stampbot/config.py:237: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_with_required_title_patterns - ValueError: required_title_patterns must contain only strings
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 25 passed, 3 deselected, 1 warning in 2.73s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 9, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -235,7 +235,7 @@
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
- if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
+ if not len(pattern) > MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters".........................F
=================================== FAILURES ===================================
________________ test_repo_config_with_required_title_patterns _________________
def test_repo_config_with_required_title_patterns():
"""Test parsing repository config with required title patterns."""
toml_content = """
required_title_patterns = ["^\\\\[bot\\\\]", "^chore:"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:295:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['^\\[bot\\]', '^chore:']
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if not len(pattern) > MAX_TITLE_PATTERN_LENGTH:
> raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"
)
E ValueError: required_title_patterns contains a pattern longer than 256 characters
stampbot/config.py:239: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_with_required_title_patterns - ValueError: required_title_patterns contains a pattern longer than 256 characters
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 25 passed, 3 deselected, 1 warning in 2.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 10, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -298,7 +298,7 @@
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
- if self.required_labels:
+ if not self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,................................F
=================================== FAILURES ===================================
________________________ test_is_pr_eligible_no_filters ________________________
def test_is_pr_eligible_no_filters():
"""Test is_pr_eligible returns True when no filters configured."""
config = RepoConfig.default()
is_eligible, reason = config.is_pr_eligible(["some-label"], "Some PR title", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:353: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_no_filters - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 32 passed, 3 deselected, 1 warning in 2.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 11, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -299,7 +299,7 @@
"""
# Check required labels filter
if self.required_labels:
- if not any(label in self.required_labels for label in pr_labels):
+ if not not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",.................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_required_label_present __________________
def test_is_pr_eligible_required_label_present():
"""Test is_pr_eligible returns True when required label is present."""
config = RepoConfig.from_toml('required_labels = ["dependencies", "automated"]')
is_eligible, reason = config.is_pr_eligible(["dependencies"], "PR title", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:361: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_required_label_present - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 33 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 12, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -306,7 +306,7 @@
)
# Check required title patterns filter
- if self._compiled_title_patterns:
+ if not self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:................................F
=================================== FAILURES ===================================
________________________ test_is_pr_eligible_no_filters ________________________
def test_is_pr_eligible_no_filters():
"""Test is_pr_eligible returns True when no filters configured."""
config = RepoConfig.default()
is_eligible, reason = config.is_pr_eligible(["some-label"], "Some PR title", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:353: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_no_filters - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 32 passed, 3 deselected, 1 warning in 2.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 13, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -307,7 +307,7 @@
# Check required title patterns filter
if self._compiled_title_patterns:
- if not isinstance(pr_title, str):
+ if not not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_matches ___________________
def test_is_pr_eligible_title_pattern_matches():
"""Test is_pr_eligible returns True when title matches pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "chore: update dependencies", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:386: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_matches - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 36 passed, 3 deselected, 1 warning in 2.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 14, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -309,7 +309,7 @@
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
- if len(pr_title) > MAX_PR_TITLE_LENGTH:
+ if not len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_matches ___________________
def test_is_pr_eligible_title_pattern_matches():
"""Test is_pr_eligible returns True when title matches pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "chore: update dependencies", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:386: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_matches - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 36 passed, 3 deselected, 1 warning in 2.81s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 15, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -317,7 +317,7 @@
for pattern in self._compiled_title_patterns:
try:
- if pattern.search(
+ if not pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,.....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_no_match __________________
def test_is_pr_eligible_title_pattern_no_match():
"""Test is_pr_eligible returns False when title doesn't match any pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "feat: add new feature", "someuser")
> assert is_eligible is False
E assert True is False
tests/test_config.py:394: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_no_match - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 37 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 16, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -336,7 +336,7 @@
return False, "PR title does not match any required pattern"
# Check allowed users/teams filter (if either is configured)
- if self.allowed_users or self.allowed_teams:
+ if not self.allowed_users or self.allowed_teams:
# User is allowed if they're in allowed_users
if pr_author in self.allowed_users:
return True, None................................F
=================================== FAILURES ===================================
________________________ test_is_pr_eligible_no_filters ________________________
def test_is_pr_eligible_no_filters():
"""Test is_pr_eligible returns True when no filters configured."""
config = RepoConfig.default()
is_eligible, reason = config.is_pr_eligible(["some-label"], "Some PR title", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:353: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_no_filters - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 32 passed, 3 deselected, 1 warning in 2.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 17, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -338,7 +338,7 @@
# Check allowed users/teams filter (if either is configured)
if self.allowed_users or self.allowed_teams:
# User is allowed if they're in allowed_users
- if pr_author in self.allowed_users:
+ if not pr_author in self.allowed_users:
return True, None
# Or if they're a member of an allowed team...............................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_user _______________________
def test_is_pr_eligible_allowed_user():
"""Test is_pr_eligible returns True when author is in allowed_users."""
config = RepoConfig.from_toml('allowed_users = ["dependabot[bot]", "renovate[bot]"]')
is_eligible, reason = config.is_pr_eligible([], "Update deps", "dependabot[bot]")
> assert is_eligible is True
E assert False is True
tests/test_config.py:505: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_user - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 47 passed, 3 deselected, 1 warning in 2.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 18, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -342,7 +342,7 @@
return True, None
# Or if they're a member of an allowed team
- if self.allowed_teams and author_team_slugs:
+ if not self.allowed_teams and author_team_slugs:
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
team_slug = team.split("/")[-1] if "/" in team else team.................................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_team _______________________
def test_is_pr_eligible_allowed_team():
"""Test is_pr_eligible returns True when author is in an allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "team-member", author_team_slugs=["release-team"]
)
> assert is_eligible is True
E assert False is True
tests/test_config.py:523: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_team - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 49 passed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 19, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -345,7 +345,7 @@
if self.allowed_teams and author_team_slugs:
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
- team_slug = team.split("/")[-1] if "/" in team else team
+ team_slug = team.split("/")[-1] if not "/" in team else team
if team_slug in author_team_slugs:
return True, None
.................................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_team _______________________
def test_is_pr_eligible_allowed_team():
"""Test is_pr_eligible returns True when author is in an allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "team-member", author_team_slugs=["release-team"]
)
> assert is_eligible is True
E assert False is True
tests/test_config.py:523: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_team - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 49 passed, 3 deselected, 1 warning in 2.72s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 20, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -346,7 +346,7 @@
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
team_slug = team.split("/")[-1] if "/" in team else team
- if team_slug in author_team_slugs:
+ if not team_slug in author_team_slugs:
return True, None
# Neither user nor team matched.................................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_team _______________________
def test_is_pr_eligible_allowed_team():
"""Test is_pr_eligible returns True when author is in an allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "team-member", author_team_slugs=["release-team"]
)
> assert is_eligible is True
E assert False is True
tests/test_config.py:523: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_team - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 49 passed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 21, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -350,7 +350,7 @@
return True, None
# Neither user nor team matched
- if self.allowed_users and self.allowed_teams:
+ if not self.allowed_users and self.allowed_teams:
return False, "PR author not in allowed users or teams"
elif self.allowed_users:
return False, f"PR author not in allowed users: {', '.join(self.allowed_users)}"..................................................F
=================================== FAILURES ===================================
_____________________ test_is_pr_eligible_team_not_matched _____________________
def test_is_pr_eligible_team_not_matched():
"""Test is_pr_eligible returns False when author is not in any allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "non-member", author_team_slugs=["other-team"]
)
assert is_eligible is False
> assert "not a member of any allowed team" in reason
E AssertionError: assert 'not a member of any allowed team' in 'PR author not in allowed users or teams'
tests/test_config.py:534: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_team_not_matched - AssertionError: assert 'not a member of any allowed team' in 'PR author not in allowed users or teams'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 50 passed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 22, definition_name: needs_team_check
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -368,7 +368,7 @@
Returns:
True if team check is needed (author not in allowed_users but teams configured)
"""
- if not self.allowed_teams:
+ if not not self.allowed_teams:
return False
if self.allowed_users and pr_author in self.allowed_users:
return False.......................................................F
=================================== FAILURES ===================================
__________________ test_needs_team_check_no_teams_configured ___________________
def test_needs_team_check_no_teams_configured():
"""Test needs_team_check returns False when no teams configured."""
config = RepoConfig.from_toml('allowed_users = ["some-user"]')
> assert config.needs_team_check("any-user") is False
E AssertionError: assert True is False
E + where True = needs_team_check('any-user')
E + where needs_team_check = <stampbot.config.RepoConfig object at 0x7fdf407df710>.needs_team_check
tests/test_config.py:595: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_needs_team_check_no_teams_configured - AssertionError: assert True is False
+ where True = needs_team_check('any-user')
+ where needs_team_check = <stampbot.config.RepoConfig object at 0x7fdf407df710>.needs_team_check
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 55 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 23, definition_name: needs_team_check
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -370,7 +370,7 @@
"""
if not self.allowed_teams:
return False
- if self.allowed_users and pr_author in self.allowed_users:
+ if not self.allowed_users and pr_author in self.allowed_users:
return False
return True
........................................................F
=================================== FAILURES ===================================
_________________ test_needs_team_check_user_in_allowed_users __________________
def test_needs_team_check_user_in_allowed_users():
"""Test needs_team_check returns False when user is in allowed_users."""
toml_content = """
allowed_users = ["special-user"]
allowed_teams = ["my-org/release-team"]
"""
config = RepoConfig.from_toml(toml_content)
> assert config.needs_team_check("special-user") is False
E AssertionError: assert True is False
E + where True = needs_team_check('special-user')
E + where needs_team_check = <stampbot.config.RepoConfig object at 0x7f47549d7710>.needs_team_check
tests/test_config.py:605: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_needs_team_check_user_in_allowed_users - AssertionError: assert True is False
+ where True = needs_team_check('special-user')
+ where needs_team_check = <stampbot.config.RepoConfig object at 0x7f47549d7710>.needs_team_check
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 56 passed, 3 deselected, 1 warning in 2.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 24, definition_name: _get_defaults
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -386,7 +386,7 @@
defaults = REPO_CONFIG_DEFAULTS.copy()
# Allow overriding defaults via settings.toml [defaults] section
- if settings.get("defaults"):
+ if not settings.get("defaults"):
settings_defaults = settings.defaults
for key in defaults:
if hasattr(settings_defaults, key):.....F
=================================== FAILURES ===================================
_ test_repo_config_default_or_config_error_fails_closed[approve_commands-approve-must be a list of strings] _
setting = 'approve_commands', value = 'approve'
message = 'must be a list of strings'
@pytest.mark.parametrize(
("setting", "value", "message"),
[
("approve_commands", "approve", "must be a list of strings"),
("unapprove_commands", ["unapprove", 1], "must contain only strings"),
(
"chatops_required_permission",
"owner",
"Invalid chatops_required_permission",
),
(
"chatops_required_permission",
["maintain"],
"Invalid chatops_required_permission",
),
],
)
def test_repo_config_default_or_config_error_fails_closed(setting, value, message):
"""Test invalid service defaults produce a non-authorizing config."""
service_defaults = MagicMock(spec=[setting])
setattr(service_defaults, setting, value)
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
config = RepoConfig.default_or_config_error()
> assert config.config_error is not None
E assert None is not None
E + where None = <stampbot.config.RepoConfig object at 0x7f8fc7fdbad0>.config_error
tests/test_config.py:109: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_default_or_config_error_fails_closed[approve_commands-approve-must be a list of strings] - assert None is not None
+ where None = <stampbot.config.RepoConfig object at 0x7f8fc7fdbad0>.config_error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 5 passed, 3 deselected, 1 warning in 2.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 25, definition_name: _get_defaults
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -389,7 +389,7 @@
if settings.get("defaults"):
settings_defaults = settings.defaults
for key in defaults:
- if hasattr(settings_defaults, key):
+ if not hasattr(settings_defaults, key):
defaults[key] = getattr(settings_defaults, key)
return defaults.....F
=================================== FAILURES ===================================
_ test_repo_config_default_or_config_error_fails_closed[approve_commands-approve-must be a list of strings] _
setting = 'approve_commands', value = 'approve'
message = 'must be a list of strings'
@pytest.mark.parametrize(
("setting", "value", "message"),
[
("approve_commands", "approve", "must be a list of strings"),
("unapprove_commands", ["unapprove", 1], "must contain only strings"),
(
"chatops_required_permission",
"owner",
"Invalid chatops_required_permission",
),
(
"chatops_required_permission",
["maintain"],
"Invalid chatops_required_permission",
),
],
)
def test_repo_config_default_or_config_error_fails_closed(setting, value, message):
"""Test invalid service defaults produce a non-authorizing config."""
service_defaults = MagicMock(spec=[setting])
setattr(service_defaults, setting, value)
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
> config = RepoConfig.default_or_config_error()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:107:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:476: in default_or_config_error
return cls.default()
^^^^^^^^^^^^^
stampbot/config.py:453: in default
defaults = cls._get_defaults()
^^^^^^^^^^^^^^^^^^^
stampbot/config.py:393: in _get_defaults
defaults[key] = getattr(settings_defaults, key)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='settings.get()' id='140186646631088'>
name = 'approval_labels'
def __getattr__(self, name):
if name in {'_mock_methods', '_mock_unsafe'}:
raise AttributeError(name)
elif self._mock_methods is not None:
if name not in self._mock_methods or name in _all_magics:
> raise AttributeError("Mock object has no attribute %r" % name)
E AttributeError: Mock object has no attribute 'approval_labels'
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:697: AttributeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_default_or_config_error_fails_closed[approve_commands-approve-must be a list of strings] - AttributeError: Mock object has no attribute 'approval_labels'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 5 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 26, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -415,7 +415,7 @@
# Parse the repo-specific TOML
repo_settings: dict[str, Any] = {}
- if toml_content:
+ if not toml_content:
try:
repo_settings = toml.loads(toml_content)
except Exception as e:F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
config = RepoConfig.from_toml(toml_content)
> assert "test" in config.approval_labels
E AssertionError: assert 'test' in ['autoapprove', 'stamp']
E + where ['autoapprove', 'stamp'] = <stampbot.config.RepoConfig object at 0x7ffb157492b0>.approval_labels
tests/test_config.py:32: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - AssertionError: assert 'test' in ['autoapprove', 'stamp']
+ where ['autoapprove', 'stamp'] = <stampbot.config.RepoConfig object at 0x7ffb157492b0>.approval_labels
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -22,7 +22,7 @@
# These can be overridden in settings.toml or per-repo stampbot.toml
REPO_CONFIG_DEFAULTS = {
"approval_labels": ["autoapprove", "stamp"],
- "auto_approve_on_label": True,
+ "auto_approve_on_label": False,
"reapprove": False,
"chatops_enabled": True,
"chatops_required_permission": "maintain",.....................F
=================================== FAILURES ===================================
_____________ test_repo_config_get_defaults_with_settings_override _____________
def test_repo_config_get_defaults_with_settings_override():
"""Test _get_defaults respects settings.toml overrides."""
with patch("stampbot.config.settings") as mock_settings:
# Create a mock defaults object with only specific attributes
# Using spec=[] means hasattr will return False for most attributes
mock_defaults = MagicMock(
spec=["approval_labels", "chatops_enabled", "chatops_required_permission"]
)
mock_defaults.approval_labels = ["custom-label"]
mock_defaults.chatops_enabled = False
mock_defaults.chatops_required_permission = "write"
mock_settings.get.return_value = mock_defaults
mock_settings.defaults = mock_defaults
defaults = RepoConfig._get_defaults()
# Should have the overridden values
assert defaults["approval_labels"] == ["custom-label"]
assert defaults["chatops_enabled"] is False
assert defaults["chatops_required_permission"] == "write"
# Non-overridden values should use REPO_CONFIG_DEFAULTS
> assert defaults["auto_approve_on_label"] is True
E assert False is True
tests/test_config.py:243: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_get_defaults_with_settings_override - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 21 passed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 1, definition_name: None
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -24,7 +24,7 @@
"approval_labels": ["autoapprove", "stamp"],
"auto_approve_on_label": True,
"reapprove": False,
- "chatops_enabled": True,
+ "chatops_enabled": False,
"chatops_required_permission": "maintain",
"approve_commands": ["approve", "stamp"],
"unapprove_commands": ["unapprove", "unstamp"],.......................F
=================================== FAILURES ===================================
________________ test_repo_config_get_defaults_partial_override ________________
def test_repo_config_get_defaults_partial_override():
"""Test _get_defaults with partial settings override."""
with patch("stampbot.config.settings") as mock_settings:
# Create a mock defaults object with only some attributes
mock_defaults = MagicMock(spec=[]) # Empty spec means no attributes
# But we can still check hasattr behavior by setting one attribute
mock_defaults.approval_labels = ["override-only-this"]
mock_settings.get.return_value = mock_defaults
mock_settings.defaults = mock_defaults
defaults = RepoConfig._get_defaults()
# The overridden value
assert defaults["approval_labels"] == ["override-only-this"]
# Non-overridden values should use REPO_CONFIG_DEFAULTS
assert defaults["auto_approve_on_label"] is True
> assert defaults["chatops_enabled"] is True
E assert False is True
tests/test_config.py:277: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_get_defaults_partial_override - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 23 passed, 3 deselected, 1 warning in 2.63s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 2, definition_name: None
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -49,7 +49,7 @@
envvar_prefix="STAMPBOT",
settings_files=["settings.toml", ".secrets.toml"],
environments=False, # We don't use [development]/[production] sections
- load_dotenv=True,
+ load_dotenv=False,
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.23s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 3, definition_name: fail_closed
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -263,7 +263,7 @@
"""
return cls(
approval_labels=["autoapprove", "stamp"],
- auto_approve_on_label=True,
+ auto_approve_on_label=False,
reapprove=False,
chatops_enabled=True,
chatops_required_permission="maintain",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 4, definition_name: fail_closed
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -265,7 +265,7 @@
approval_labels=["autoapprove", "stamp"],
auto_approve_on_label=True,
reapprove=False,
- chatops_enabled=True,
+ chatops_enabled=False,
chatops_required_permission="maintain",
approve_commands=["approve", "stamp"],
unapprove_commands=["unapprove", "unstamp"],........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.09s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 5, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -320,7 +320,7 @@
if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
- concurrent=True,
+ concurrent=False,
):
break
except TimeoutError:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 6, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -339,7 +339,7 @@
if self.allowed_users or self.allowed_teams:
# User is allowed if they're in allowed_users
if pr_author in self.allowed_users:
- return True, None
+ return False, None
# Or if they're a member of an allowed team
if self.allowed_teams and author_team_slugs:...............................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_user _______________________
def test_is_pr_eligible_allowed_user():
"""Test is_pr_eligible returns True when author is in allowed_users."""
config = RepoConfig.from_toml('allowed_users = ["dependabot[bot]", "renovate[bot]"]')
is_eligible, reason = config.is_pr_eligible([], "Update deps", "dependabot[bot]")
> assert is_eligible is True
E assert False is True
tests/test_config.py:505: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_user - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 47 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 7, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -347,7 +347,7 @@
# Support both "org/team" and "team" formats
team_slug = team.split("/")[-1] if "/" in team else team
if team_slug in author_team_slugs:
- return True, None
+ return False, None
# Neither user nor team matched
if self.allowed_users and self.allowed_teams:.................................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_team _______________________
def test_is_pr_eligible_allowed_team():
"""Test is_pr_eligible returns True when author is in an allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "team-member", author_team_slugs=["release-team"]
)
> assert is_eligible is True
E assert False is True
tests/test_config.py:523: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_team - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 49 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 8, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -357,7 +357,7 @@
else:
return False, "PR author not a member of any allowed team"
- return True, None
+ return False, None
def needs_team_check(self, pr_author: str) -> bool:
"""Check if team membership verification is needed for this author.................................F
=================================== FAILURES ===================================
________________________ test_is_pr_eligible_no_filters ________________________
def test_is_pr_eligible_no_filters():
"""Test is_pr_eligible returns True when no filters configured."""
config = RepoConfig.default()
is_eligible, reason = config.is_pr_eligible(["some-label"], "Some PR title", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:353: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_no_filters - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 32 passed, 3 deselected, 1 warning in 2.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 9, definition_name: needs_team_check
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -372,7 +372,7 @@
return False
if self.allowed_users and pr_author in self.allowed_users:
return False
- return True
+ return False
@classmethod
def _get_defaults(cls) -> dict[str, Any]:.........................................................F
=================================== FAILURES ===================================
_______________ test_needs_team_check_user_not_in_allowed_users ________________
def test_needs_team_check_user_not_in_allowed_users():
"""Test needs_team_check returns True when user not in allowed_users but teams configured."""
toml_content = """
allowed_users = ["special-user"]
allowed_teams = ["my-org/release-team"]
"""
config = RepoConfig.from_toml(toml_content)
> assert config.needs_team_check("other-user") is True
E AssertionError: assert False is True
E + where False = needs_team_check('other-user')
E + where needs_team_check = <stampbot.config.RepoConfig object at 0x7f872b2e7710>.needs_team_check
tests/test_config.py:615: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_needs_team_check_user_not_in_allowed_users - AssertionError: assert False is True
+ where False = needs_team_check('other-user')
+ where needs_team_check = <stampbot.config.RepoConfig object at 0x7f872b2e7710>.needs_team_check
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 57 passed, 3 deselected, 1 warning in 2.88s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 10, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -428,7 +428,7 @@
return cls(
approval_labels=merged.get("approval_labels", []),
- auto_approve_on_label=merged.get("auto_approve_on_label", True),
+ auto_approve_on_label=merged.get("auto_approve_on_label", False),
reapprove=merged.get("reapprove", False),
chatops_enabled=merged.get("chatops_enabled", True),
chatops_required_permission=merged.get(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceTrueWithFalse, occurrence: 11, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -430,7 +430,7 @@
approval_labels=merged.get("approval_labels", []),
auto_approve_on_label=merged.get("auto_approve_on_label", True),
reapprove=merged.get("reapprove", False),
- chatops_enabled=merged.get("chatops_enabled", True),
+ chatops_enabled=merged.get("chatops_enabled", False),
chatops_required_permission=merged.get(
"chatops_required_permission",
"maintain",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.05s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -23,7 +23,7 @@
REPO_CONFIG_DEFAULTS = {
"approval_labels": ["autoapprove", "stamp"],
"auto_approve_on_label": True,
- "reapprove": False,
+ "reapprove": True,
"chatops_enabled": True,
"chatops_required_permission": "maintain",
"approve_commands": ["approve", "stamp"],........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 1, definition_name: None
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -48,7 +48,7 @@
settings = Dynaconf(
envvar_prefix="STAMPBOT",
settings_files=["settings.toml", ".secrets.toml"],
- environments=False, # We don't use [development]/[production] sections
+ environments=True, # We don't use [development]/[production] sections
load_dotenv=True,
)
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:51: in <module>
configure_logging()
stampbot/logger.py:57: in configure_logging
log_level_int = logging.getLevelName(settings.log_level)
^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/dynaconf/base.py:98: in __getattr__
return getattr(self._wrapped, name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/dynaconf/base.py:372: in __getattr__
raise AttributeError(key)
E AttributeError: log_level
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - AttributeError: log_level
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.19s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 2, definition_name: fail_closed
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -264,7 +264,7 @@
return cls(
approval_labels=["autoapprove", "stamp"],
auto_approve_on_label=True,
- reapprove=False,
+ reapprove=True,
chatops_enabled=True,
chatops_required_permission="maintain",
approve_commands=["approve", "stamp"],........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.23s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 3, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -301,7 +301,7 @@
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
- False,
+ True,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
..................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_required_label_missing __________________
def test_is_pr_eligible_required_label_missing():
"""Test is_pr_eligible returns False when required label is missing."""
config = RepoConfig.from_toml('required_labels = ["dependencies", "automated"]')
is_eligible, reason = config.is_pr_eligible(["other-label"], "PR title", "someuser")
> assert is_eligible is False
E assert True is False
tests/test_config.py:369: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_required_label_missing - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 34 passed, 3 deselected, 1 warning in 2.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 4, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -308,7 +308,7 @@
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
- return False, "PR title is not valid text"
+ return True, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,.........................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_rejects_non_text_title __________________
def test_is_pr_eligible_rejects_non_text_title():
"""Test malformed webhook title input fails closed."""
config = RepoConfig.from_toml('required_title_patterns = [".*"]')
is_eligible, reason = config.is_pr_eligible([], None, "someuser") # type: ignore[arg-type]
> assert is_eligible is False
E assert True is False
tests/test_config.py:437: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_rejects_non_text_title - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 41 passed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 5, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -311,7 +311,7 @@
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
- False,
+ True,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
........................................F
=================================== FAILURES ===================================
__________ test_is_pr_eligible_rejects_overlong_title_before_matching __________
def test_is_pr_eligible_rejects_overlong_title_before_matching():
"""Test webhook title input is bounded before regex evaluation."""
config = RepoConfig.from_toml('required_title_patterns = [".*"]')
config._compiled_title_patterns[0] = MagicMock()
is_eligible, reason = config.is_pr_eligible([], "a" * (MAX_PR_TITLE_LENGTH + 1), "someuser")
> assert is_eligible is False
E assert True is False
tests/test_config.py:426: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_rejects_overlong_title_before_matching - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 40 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 6, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -326,7 +326,7 @@
except TimeoutError:
timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
return (
- False,
+ True,
"PR title pattern evaluation could not complete within "
f"the {timeout_ms} ms safety limit",
)..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
> assert is_eligible is False
E assert True is False
tests/test_config.py:449: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 7, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -331,7 +331,7 @@
f"the {timeout_ms} ms safety limit",
)
except regex.error:
- return False, "PR title pattern evaluation failed safely"
+ return True, "PR title pattern evaluation failed safely"
else:
return False, "PR title does not match any required pattern"
...........................................F
=================================== FAILURES ===================================
______________ test_is_pr_eligible_regex_engine_failure_is_closed ______________
def test_is_pr_eligible_regex_engine_failure_is_closed():
"""Test a regex engine failure cannot approve a pull request."""
config = RepoConfig.from_toml('required_title_patterns = ["^fix:"]')
config._compiled_title_patterns[0] = MagicMock()
config._compiled_title_patterns[0].search.side_effect = regex.error("engine failure")
is_eligible, reason = config.is_pr_eligible([], "fix: dependency", "someuser")
> assert is_eligible is False
E assert True is False
tests/test_config.py:461: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_regex_engine_failure_is_closed - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 43 passed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 8, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -333,7 +333,7 @@
except regex.error:
return False, "PR title pattern evaluation failed safely"
else:
- return False, "PR title does not match any required pattern"
+ return True, "PR title does not match any required pattern"
# Check allowed users/teams filter (if either is configured)
if self.allowed_users or self.allowed_teams:.....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_no_match __________________
def test_is_pr_eligible_title_pattern_no_match():
"""Test is_pr_eligible returns False when title doesn't match any pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "feat: add new feature", "someuser")
> assert is_eligible is False
E assert True is False
tests/test_config.py:394: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_no_match - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 37 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 9, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -351,7 +351,7 @@
# Neither user nor team matched
if self.allowed_users and self.allowed_teams:
- return False, "PR author not in allowed users or teams"
+ return True, "PR author not in allowed users or teams"
elif self.allowed_users:
return False, f"PR author not in allowed users: {', '.join(self.allowed_users)}"
else:....................................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_neither_user_nor_team ___________________
def test_is_pr_eligible_neither_user_nor_team():
"""Test is_pr_eligible returns False when neither user nor team matches."""
toml_content = """
allowed_users = ["special-user"]
allowed_teams = ["my-org/release-team"]
"""
config = RepoConfig.from_toml(toml_content)
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "random-user", author_team_slugs=["other-team"]
)
> assert is_eligible is False
E assert True is False
tests/test_config.py:560: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_neither_user_nor_team - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 52 passed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 10, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -353,7 +353,7 @@
if self.allowed_users and self.allowed_teams:
return False, "PR author not in allowed users or teams"
elif self.allowed_users:
- return False, f"PR author not in allowed users: {', '.join(self.allowed_users)}"
+ return True, f"PR author not in allowed users: {', '.join(self.allowed_users)}"
else:
return False, "PR author not a member of any allowed team"
................................................F
=================================== FAILURES ===================================
_____________________ test_is_pr_eligible_user_not_allowed _____________________
def test_is_pr_eligible_user_not_allowed():
"""Test is_pr_eligible returns False when author is not in allowed_users."""
config = RepoConfig.from_toml('allowed_users = ["dependabot[bot]", "renovate[bot]"]')
is_eligible, reason = config.is_pr_eligible([], "Update deps", "random-user")
> assert is_eligible is False
E assert True is False
tests/test_config.py:513: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_user_not_allowed - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 48 passed, 3 deselected, 1 warning in 2.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 11, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -355,7 +355,7 @@
elif self.allowed_users:
return False, f"PR author not in allowed users: {', '.join(self.allowed_users)}"
else:
- return False, "PR author not a member of any allowed team"
+ return True, "PR author not a member of any allowed team"
return True, None
..................................................F
=================================== FAILURES ===================================
_____________________ test_is_pr_eligible_team_not_matched _____________________
def test_is_pr_eligible_team_not_matched():
"""Test is_pr_eligible returns False when author is not in any allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "non-member", author_team_slugs=["other-team"]
)
> assert is_eligible is False
E assert True is False
tests/test_config.py:533: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_team_not_matched - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 50 passed, 3 deselected, 1 warning in 2.72s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 12, definition_name: needs_team_check
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -369,7 +369,7 @@
True if team check is needed (author not in allowed_users but teams configured)
"""
if not self.allowed_teams:
- return False
+ return True
if self.allowed_users and pr_author in self.allowed_users:
return False
return True.......................................................F
=================================== FAILURES ===================================
__________________ test_needs_team_check_no_teams_configured ___________________
def test_needs_team_check_no_teams_configured():
"""Test needs_team_check returns False when no teams configured."""
config = RepoConfig.from_toml('allowed_users = ["some-user"]')
> assert config.needs_team_check("any-user") is False
E AssertionError: assert True is False
E + where True = needs_team_check('any-user')
E + where needs_team_check = <stampbot.config.RepoConfig object at 0x7fc242967710>.needs_team_check
tests/test_config.py:595: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_needs_team_check_no_teams_configured - AssertionError: assert True is False
+ where True = needs_team_check('any-user')
+ where needs_team_check = <stampbot.config.RepoConfig object at 0x7fc242967710>.needs_team_check
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 55 passed, 3 deselected, 1 warning in 2.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 13, definition_name: needs_team_check
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -371,7 +371,7 @@
if not self.allowed_teams:
return False
if self.allowed_users and pr_author in self.allowed_users:
- return False
+ return True
return True
@classmethod........................................................F
=================================== FAILURES ===================================
_________________ test_needs_team_check_user_in_allowed_users __________________
def test_needs_team_check_user_in_allowed_users():
"""Test needs_team_check returns False when user is in allowed_users."""
toml_content = """
allowed_users = ["special-user"]
allowed_teams = ["my-org/release-team"]
"""
config = RepoConfig.from_toml(toml_content)
> assert config.needs_team_check("special-user") is False
E AssertionError: assert True is False
E + where True = needs_team_check('special-user')
E + where needs_team_check = <stampbot.config.RepoConfig object at 0x7fbcb965b710>.needs_team_check
tests/test_config.py:605: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_needs_team_check_user_in_allowed_users - AssertionError: assert True is False
+ where True = needs_team_check('special-user')
+ where needs_team_check = <stampbot.config.RepoConfig object at 0x7fbcb965b710>.needs_team_check
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 56 passed, 3 deselected, 1 warning in 2.67s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 14, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -429,7 +429,7 @@
return cls(
approval_labels=merged.get("approval_labels", []),
auto_approve_on_label=merged.get("auto_approve_on_label", True),
- reapprove=merged.get("reapprove", False),
+ reapprove=merged.get("reapprove", True),
chatops_enabled=merged.get("chatops_enabled", True),
chatops_required_permission=merged.get(
"chatops_required_permission",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -84,7 +84,7 @@
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
- if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
+ if isinstance(raw, bool) or (isinstance(raw, float) or not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)...........................................................F
=================================== FAILURES ===================================
___________ test_repo_config_cache_seconds_accepts_integers[120-120] ___________
raw = '120', expected = 120
@pytest.mark.parametrize(
("raw", "expected"),
[(300, 300), ("120", 120), (0, 0), (5.0, 5)],
)
def test_repo_config_cache_seconds_accepts_integers(raw, expected):
"""Test the cache lifetime accepts integer-valued settings."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = raw
> assert repo_config_cache_seconds() == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:627:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def repo_config_cache_seconds() -> int:
"""Return how long a repository's parsed policy stays cached, in seconds.
Returns:
A non-negative integer. Zero disables the cache.
Raises:
ValueError: If ``repo_config_cache_seconds`` is not a non-negative integer.
"""
raw = settings.get("repo_config_cache_seconds", DEFAULT_REPO_CONFIG_CACHE_SECONDS)
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
> if isinstance(raw, bool) or (isinstance(raw, float) or not raw.is_integer()):
^^^^^^^^^^^^^^
E AttributeError: 'str' object has no attribute 'is_integer'
stampbot/config.py:87: AttributeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_accepts_integers[120-120] - AttributeError: 'str' object has no attribute 'is_integer'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 59 passed, 3 deselected, 1 warning in 2.67s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceAndWithOr, occurrence: 1, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -342,7 +342,7 @@
return True, None
# Or if they're a member of an allowed team
- if self.allowed_teams and author_team_slugs:
+ if self.allowed_teams or author_team_slugs:
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
team_slug = team.split("/")[-1] if "/" in team else team........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 2, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -350,7 +350,7 @@
return True, None
# Neither user nor team matched
- if self.allowed_users and self.allowed_teams:
+ if self.allowed_users or self.allowed_teams:
return False, "PR author not in allowed users or teams"
elif self.allowed_users:
return False, f"PR author not in allowed users: {', '.join(self.allowed_users)}"..................................................F
=================================== FAILURES ===================================
_____________________ test_is_pr_eligible_team_not_matched _____________________
def test_is_pr_eligible_team_not_matched():
"""Test is_pr_eligible returns False when author is not in any allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "non-member", author_team_slugs=["other-team"]
)
assert is_eligible is False
> assert "not a member of any allowed team" in reason
E AssertionError: assert 'not a member of any allowed team' in 'PR author not in allowed users or teams'
tests/test_config.py:534: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_team_not_matched - AssertionError: assert 'not a member of any allowed team' in 'PR author not in allowed users or teams'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 50 passed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 3, definition_name: needs_team_check
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -370,7 +370,7 @@
"""
if not self.allowed_teams:
return False
- if self.allowed_users and pr_author in self.allowed_users:
+ if self.allowed_users or pr_author in self.allowed_users:
return False
return True
.........................................................F
=================================== FAILURES ===================================
_______________ test_needs_team_check_user_not_in_allowed_users ________________
def test_needs_team_check_user_not_in_allowed_users():
"""Test needs_team_check returns True when user not in allowed_users but teams configured."""
toml_content = """
allowed_users = ["special-user"]
allowed_teams = ["my-org/release-team"]
"""
config = RepoConfig.from_toml(toml_content)
> assert config.needs_team_check("other-user") is True
E AssertionError: assert False is True
E + where False = needs_team_check('other-user')
E + where needs_team_check = <stampbot.config.RepoConfig object at 0x7f29703df710>.needs_team_check
tests/test_config.py:615: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_needs_team_check_user_not_in_allowed_users - AssertionError: assert False is True
+ where False = needs_team_check('other-user')
+ where needs_team_check = <stampbot.config.RepoConfig object at 0x7f29703df710>.needs_team_check
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 57 passed, 3 deselected, 1 warning in 2.73s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -84,7 +84,7 @@
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
- if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
+ if isinstance(raw, bool) and (isinstance(raw, float) and not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)....................................................................F
=================================== FAILURES ===================================
_________ test_repo_config_cache_seconds_rejects_invalid_values[True] __________
raw = True
@pytest.mark.parametrize("raw", ["soon", None, -5, "-1", [300], True, False, 1.9, "1.9"])
def test_repo_config_cache_seconds_rejects_invalid_values(raw):
"""Test a non-integer or negative lifetime fails at startup instead of guessing."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = raw
> with pytest.raises(ValueError, match="repo_config_cache_seconds"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_config.py:644: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_rejects_invalid_values[True] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 68 passed, 3 deselected, 1 warning in 2.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 1, definition_name: __init__
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -161,7 +161,7 @@
unapprove_commands,
"unapprove_commands",
)
- self.required_labels = required_labels or []
+ self.required_labels = required_labels and []
title_patterns = required_title_patterns if required_title_patterns is not None else []
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
self.required_title_patterns = list(title_patterns)........................F
=================================== FAILURES ===================================
____________________ test_repo_config_with_required_labels _____________________
def test_repo_config_with_required_labels():
"""Test parsing repository config with required labels."""
toml_content = """
required_labels = ["dependencies", "automated"]
"""
config = RepoConfig.from_toml(toml_content)
> assert config.required_labels == ["dependencies", "automated"]
E AssertionError: assert [] == ['dependencies', 'automated']
E
E Right contains 2 more items, first extra item: 'dependencies'
E
E Full diff:
E + []
E - [
E - 'dependencies',
E - 'automated',
E - ]
tests/test_config.py:287: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_with_required_labels - AssertionError: assert [] == ['dependencies', 'automated']
Right contains 2 more items, first extra item: 'dependencies'
Full diff:
+ []
- [
- 'dependencies',
- 'automated',
- ]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 24 passed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 2, definition_name: __init__
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -165,7 +165,7 @@
title_patterns = required_title_patterns if required_title_patterns is not None else []
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
self.required_title_patterns = list(title_patterns)
- self.allowed_users = allowed_users or []
+ self.allowed_users = allowed_users and []
self.allowed_teams = allowed_teams or []
self.config_error = config_error
................................................F
=================================== FAILURES ===================================
_____________________ test_is_pr_eligible_user_not_allowed _____________________
def test_is_pr_eligible_user_not_allowed():
"""Test is_pr_eligible returns False when author is not in allowed_users."""
config = RepoConfig.from_toml('allowed_users = ["dependabot[bot]", "renovate[bot]"]')
is_eligible, reason = config.is_pr_eligible([], "Update deps", "random-user")
> assert is_eligible is False
E assert True is False
tests/test_config.py:513: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_user_not_allowed - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 48 passed, 3 deselected, 1 warning in 2.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 3, definition_name: __init__
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -166,7 +166,7 @@
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
self.required_title_patterns = list(title_patterns)
self.allowed_users = allowed_users or []
- self.allowed_teams = allowed_teams or []
+ self.allowed_teams = allowed_teams and []
self.config_error = config_error
@staticmethod..................................................F
=================================== FAILURES ===================================
_____________________ test_is_pr_eligible_team_not_matched _____________________
def test_is_pr_eligible_team_not_matched():
"""Test is_pr_eligible returns False when author is not in any allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "non-member", author_team_slugs=["other-team"]
)
> assert is_eligible is False
E assert True is False
tests/test_config.py:533: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_team_not_matched - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 50 passed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 4, definition_name: _validate_required_permission
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -202,7 +202,7 @@
Raises:
ValueError: If the permission is not one of GitHub's repository levels.
"""
- if not isinstance(permission, str) or permission not in REPO_PERMISSION_LEVELS:
+ if not isinstance(permission, str) and permission not in REPO_PERMISSION_LEVELS:
raise ValueError(
"Invalid chatops_required_permission: "
f"{permission}. ".......F
=================================== FAILURES ===================================
_ test_repo_config_default_or_config_error_fails_closed[chatops_required_permission-owner-Invalid chatops_required_permission] _
setting = 'chatops_required_permission', value = 'owner'
message = 'Invalid chatops_required_permission'
@pytest.mark.parametrize(
("setting", "value", "message"),
[
("approve_commands", "approve", "must be a list of strings"),
("unapprove_commands", ["unapprove", 1], "must contain only strings"),
(
"chatops_required_permission",
"owner",
"Invalid chatops_required_permission",
),
(
"chatops_required_permission",
["maintain"],
"Invalid chatops_required_permission",
),
],
)
def test_repo_config_default_or_config_error_fails_closed(setting, value, message):
"""Test invalid service defaults produce a non-authorizing config."""
service_defaults = MagicMock(spec=[setting])
setattr(service_defaults, setting, value)
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
config = RepoConfig.default_or_config_error()
> assert config.config_error is not None
E assert None is not None
E + where None = <stampbot.config.RepoConfig object at 0x7fbea8f25050>.config_error
tests/test_config.py:109: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_default_or_config_error_fails_closed[chatops_required_permission-owner-Invalid chatops_required_permission] - assert None is not None
+ where None = <stampbot.config.RepoConfig object at 0x7fbea8f25050>.config_error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 7 passed, 3 deselected, 1 warning in 2.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 5, definition_name: fail_closed
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -273,7 +273,7 @@
required_title_patterns=[],
allowed_users=[],
allowed_teams=[],
- config_error=message or "Invalid configuration",
+ config_error=message and "Invalid configuration",
)
def is_pr_eligible(..F
=================================== FAILURES ===================================
_________ test_repo_config_fail_closed_does_not_read_service_defaults __________
def test_repo_config_fail_closed_does_not_read_service_defaults():
"""Test fail-closed config is independent of invalid service settings."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.side_effect = AssertionError("service settings must not be read")
config = RepoConfig.fail_closed("invalid service configuration")
> assert config.config_error == "invalid service configuration"
E AssertionError: assert 'Invalid configuration' == 'invalid serv...configuration'
E
E - invalid service configuration
E ? ^ --------
E + Invalid configuration
E ? ^
tests/test_config.py:61: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_fail_closed_does_not_read_service_defaults - AssertionError: assert 'Invalid configuration' == 'invalid serv...configuration'
- invalid service configuration
? ^ --------
+ Invalid configuration
? ^
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 2 passed, 3 deselected, 1 warning in 2.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 6, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -336,7 +336,7 @@
return False, "PR title does not match any required pattern"
# Check allowed users/teams filter (if either is configured)
- if self.allowed_users or self.allowed_teams:
+ if self.allowed_users and self.allowed_teams:
# User is allowed if they're in allowed_users
if pr_author in self.allowed_users:
return True, None................................................F
=================================== FAILURES ===================================
_____________________ test_is_pr_eligible_user_not_allowed _____________________
def test_is_pr_eligible_user_not_allowed():
"""Test is_pr_eligible returns False when author is not in allowed_users."""
config = RepoConfig.from_toml('allowed_users = ["dependabot[bot]", "renovate[bot]"]')
is_eligible, reason = config.is_pr_eligible([], "Update deps", "random-user")
> assert is_eligible is False
E assert True is False
tests/test_config.py:513: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_user_not_allowed - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 48 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceBreakWithContinue, occurrence: 0, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -322,7 +322,7 @@
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
- break
+ continue
except TimeoutError:
timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
return (....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_matches ___________________
def test_is_pr_eligible_title_pattern_matches():
"""Test is_pr_eligible returns True when title matches pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "chore: update dependencies", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:386: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_matches - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 36 passed, 3 deselected, 1 warning in 2.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 0, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -88,7 +88,7 @@
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)
- except (TypeError, ValueError) as error:
+ except (CosmicRayTestingException, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
if seconds < 0:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")...............................................................F
=================================== FAILURES ===================================
_________ test_repo_config_cache_seconds_rejects_invalid_values[soon] __________
def repo_config_cache_seconds() -> int:
"""Return how long a repository's parsed policy stays cached, in seconds.
Returns:
A non-negative integer. Zero disables the cache.
Raises:
ValueError: If ``repo_config_cache_seconds`` is not a non-negative integer.
"""
raw = settings.get("repo_config_cache_seconds", DEFAULT_REPO_CONFIG_CACHE_SECONDS)
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
> seconds = int(raw)
^^^^^^^^
E ValueError: invalid literal for int() with base 10: 'soon'
stampbot/config.py:90: ValueError
During handling of the above exception, another exception occurred:
raw = 'soon'
@pytest.mark.parametrize("raw", ["soon", None, -5, "-1", [300], True, False, 1.9, "1.9"])
def test_repo_config_cache_seconds_rejects_invalid_values(raw):
"""Test a non-integer or negative lifetime fails at startup instead of guessing."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = raw
with pytest.raises(ValueError, match="repo_config_cache_seconds"):
> repo_config_cache_seconds()
tests/test_config.py:645:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def repo_config_cache_seconds() -> int:
"""Return how long a repository's parsed policy stays cached, in seconds.
Returns:
A non-negative integer. Zero disables the cache.
Raises:
ValueError: If ``repo_config_cache_seconds`` is not a non-negative integer.
"""
raw = settings.get("repo_config_cache_seconds", DEFAULT_REPO_CONFIG_CACHE_SECONDS)
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)
> except (CosmicRayTestingException, ValueError) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/config.py:91: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_rejects_invalid_values[soon] - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 63 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 1, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -88,7 +88,7 @@
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)
- except (TypeError, ValueError) as error:
+ except (TypeError, CosmicRayTestingException) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
if seconds < 0:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")...............................................................F
=================================== FAILURES ===================================
_________ test_repo_config_cache_seconds_rejects_invalid_values[soon] __________
def repo_config_cache_seconds() -> int:
"""Return how long a repository's parsed policy stays cached, in seconds.
Returns:
A non-negative integer. Zero disables the cache.
Raises:
ValueError: If ``repo_config_cache_seconds`` is not a non-negative integer.
"""
raw = settings.get("repo_config_cache_seconds", DEFAULT_REPO_CONFIG_CACHE_SECONDS)
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
> seconds = int(raw)
^^^^^^^^
E ValueError: invalid literal for int() with base 10: 'soon'
stampbot/config.py:90: ValueError
During handling of the above exception, another exception occurred:
raw = 'soon'
@pytest.mark.parametrize("raw", ["soon", None, -5, "-1", [300], True, False, 1.9, "1.9"])
def test_repo_config_cache_seconds_rejects_invalid_values(raw):
"""Test a non-integer or negative lifetime fails at startup instead of guessing."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = raw
with pytest.raises(ValueError, match="repo_config_cache_seconds"):
> repo_config_cache_seconds()
tests/test_config.py:645:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def repo_config_cache_seconds() -> int:
"""Return how long a repository's parsed policy stays cached, in seconds.
Returns:
A non-negative integer. Zero disables the cache.
Raises:
ValueError: If ``repo_config_cache_seconds`` is not a non-negative integer.
"""
raw = settings.get("repo_config_cache_seconds", DEFAULT_REPO_CONFIG_CACHE_SECONDS)
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)
> except (TypeError, CosmicRayTestingException) as error:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/config.py:91: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_rejects_invalid_values[soon] - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 63 passed, 3 deselected, 1 warning in 2.78s
worker outcome: WorkerOutcome.EXCEPTION
test outcome: TestOutcome.INCOMPETENT
operator: core/ExceptionReplacer, occurrence: 2, definition_name: _compile_title_patterns
Traceback (most recent call last):
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/mutating.py", line 62, in mutate_and_test
(previous_code, mutated_code) = stack.enter_context(
~~~~~~~~~~~~~~~~~~~^
use_mutation(mutation.module_path, operator, mutation.occurrence)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py", line 530, in enter_context
result = _enter(cm)
File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py", line 141, in __enter__
return next(self.gen)
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/mutating.py", line 114, in use_mutation
original_code, mutated_code = apply_mutation(module_path, operator, occurrence)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/mutating.py", line 130, in apply_mutation
return MutationVisitor.mutate_path(module_path, operator, occurrence)
~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/mutating.py", line 183, in mutate_path
mutated_code = cls.mutate_code(original_code, operator, occurrence)
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/mutating.py", line 163, in mutate_code
mutated_ast = visitor.walk(ast)
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/ast/__init__.py", line 28, in walk
node.children = [child for child in walked if child is not None]
^^^^^^
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/ast/__init__.py", line 28, in walk
node.children = [child for child in walked if child is not None]
^^^^^^
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/ast/__init__.py", line 28, in walk
node.children = [child for child in walked if child is not None]
^^^^^^
[Previous line repeated 6 more times]
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/ast/__init__.py", line 21, in walk
node = self.visit(node)
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/mutating.py", line 209, in visit
node = self.operator.mutate(node, index)
File "/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/cosmic_ray/operators/exception_replacer.py", line 25, in mutate
name_nodes[index].value = CosmicRayTestingException.__name__
^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'PythonNode' object has no attribute 'value' and no __dict__ for setting new attributes
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 3, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -242,7 +242,7 @@
)
try:
compiled_patterns.append(regex.compile(pattern, regex.VERSION0))
- except (regex.error, OverflowError) as e:
+ except (regex.error, CosmicRayTestingException) as e:
raise ValueError(f"Invalid regex pattern '{pattern}': {e}") from e
return compiled_patterns..........................F
=================================== FAILURES ===================================
____________________ test_repo_config_invalid_regex_pattern ____________________
patterns = ['[invalid']
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"
)
try:
> compiled_patterns.append(regex.compile(pattern, regex.VERSION0))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/config.py:244:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/regex/_main.py:363: in compile
return _compile(pattern, flags, ignore_unused, kwargs, cache_pattern)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pattern = '[invalid', flags = regex.V0, ignore_unused = False, kwargs = {}
cache_it = True
def _compile(pattern, flags, ignore_unused, kwargs, cache_it):
"Compiles a regular expression to a PatternObject."
global DEFAULT_VERSION
try:
from regex import DEFAULT_VERSION
except ImportError:
pass
# We won't bother to cache the pattern if we're debugging.
if (flags & DEBUG) != 0:
cache_it = False
# What locale is this pattern using?
locale_key = (type(pattern), pattern)
if _locale_sensitive.get(locale_key, True) or (flags & LOCALE) != 0:
# This pattern is, or might be, locale-sensitive.
pattern_locale = _getpreferredencoding(False)
else:
# This pattern is definitely not locale-sensitive.
pattern_locale = None
def complain_unused_args():
if ignore_unused:
return
# Complain about any unused keyword arguments, possibly resulting from a typo.
unused_kwargs = set(kwargs) - {k for k, v in args_needed}
if unused_kwargs:
any_one = next(iter(unused_kwargs))
raise ValueError('unused keyword argument {!a}'.format(any_one))
if cache_it:
try:
# Do we know what keyword arguments are needed?
args_key = pattern, type(pattern), flags
args_needed = _named_args[args_key]
# Are we being provided with its required keyword arguments?
args_supplied = set()
if args_needed:
for k, v in args_needed:
try:
args_supplied.add((k, frozenset(kwargs[k])))
except KeyError:
raise error("missing named list: {!r}".format(k))
complain_unused_args()
args_supplied = frozenset(args_supplied)
# Have we already seen this regular expression and named list?
pattern_key = (pattern, type(pattern), flags, args_supplied,
DEFAULT_VERSION, pattern_locale)
return _cache[pattern_key]
except KeyError:
# It's a new pattern, or new named list for a known pattern.
pass
# Guess the encoding from the class of the pattern string.
if isinstance(pattern, str):
guess_encoding = UNICODE
elif isinstance(pattern, bytes):
guess_encoding = ASCII
elif isinstance(pattern, Pattern):
if flags:
raise ValueError("cannot process flags argument with a compiled pattern")
return pattern
else:
raise TypeError("first argument must be a string or compiled pattern")
# Set the default version in the core code in case it has been changed.
_regex_core.DEFAULT_VERSION = DEFAULT_VERSION
global_flags = flags
while True:
caught_exception = None
try:
source = _Source(pattern)
info = _Info(global_flags, source.char_type, kwargs)
info.guess_encoding = guess_encoding
source.ignore_space = bool(info.flags & VERBOSE)
parsed = _parse_pattern(source, info)
break
except _UnscopedFlagSet:
# Remember the global flags for the next attempt.
global_flags = info.global_flags
except error as e:
caught_exception = e
if caught_exception:
> raise error(caught_exception.msg, caught_exception.pattern,
caught_exception.pos)
E regex._regex_core.error: unterminated character set at position 8
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/regex/_main.py:553: error
During handling of the above exception, another exception occurred:
def test_repo_config_invalid_regex_pattern():
"""Test invalid regex pattern raises a ValueError."""
toml_content = """
required_title_patterns = ["[invalid"]
"""
with pytest.raises(ValueError, match="Invalid regex pattern"):
> RepoConfig.from_toml(toml_content)
tests/test_config.py:307:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:429: in from_toml
return cls(
stampbot/config.py:166: in __init__
self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
patterns = ['[invalid']
@staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
Args:
patterns: Configured title patterns.
Returns:
Patterns compiled in Python ``re`` compatibility mode.
Raises:
ValueError: If the pattern list, an item, or a pattern is invalid.
"""
if not isinstance(patterns, list):
raise ValueError("required_title_patterns must be a list of strings")
if len(patterns) > MAX_TITLE_PATTERN_COUNT:
raise ValueError(
"required_title_patterns contains too many patterns "
f"(maximum: {MAX_TITLE_PATTERN_COUNT})"
)
compiled_patterns = []
for pattern in patterns:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) > MAX_TITLE_PATTERN_LENGTH:
raise ValueError(
"required_title_patterns contains a pattern longer than "
f"{MAX_TITLE_PATTERN_LENGTH} characters"
)
try:
compiled_patterns.append(regex.compile(pattern, regex.VERSION0))
> except (regex.error, CosmicRayTestingException) as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/config.py:245: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_invalid_regex_pattern - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 26 passed, 3 deselected, 1 warning in 2.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 4, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -323,7 +323,7 @@
concurrent=True,
):
break
- except TimeoutError:
+ except CosmicRayTestingException:
timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
return (
False,..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
self = <stampbot.config.RepoConfig object at 0x7fa59bde7890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
> if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
E TimeoutError: regex timed out
stampbot/config.py:320: TimeoutError
During handling of the above exception, another exception occurred:
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
> is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
tests/test_config.py:445:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7fa59bde7890>, pr_labels = []
pr_title = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
break
> except CosmicRayTestingException:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/config.py:326: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.71s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 5, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -330,7 +330,7 @@
"PR title pattern evaluation could not complete within "
f"the {timeout_ms} ms safety limit",
)
- except regex.error:
+ except regexCosmicRayTestingExceptionerror:
return False, "PR title pattern evaluation failed safely"
else:
return False, "PR title does not match any required pattern"...........................................F
=================================== FAILURES ===================================
______________ test_is_pr_eligible_regex_engine_failure_is_closed ______________
self = <stampbot.config.RepoConfig object at 0x7f0a7861b7d0>, pr_labels = []
pr_title = 'fix: dependency', pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
> if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
stampbot/config.py:320:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='mock.search' id='139682944262768'>
args = ('fix: dependency',), kwargs = {'timeout': 0.01, 'concurrent': True}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E regex._regex_core.error: engine failure
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: error
During handling of the above exception, another exception occurred:
def test_is_pr_eligible_regex_engine_failure_is_closed():
"""Test a regex engine failure cannot approve a pull request."""
config = RepoConfig.from_toml('required_title_patterns = ["^fix:"]')
config._compiled_title_patterns[0] = MagicMock()
config._compiled_title_patterns[0].search.side_effect = regex.error("engine failure")
> is_eligible, reason = config.is_pr_eligible([], "fix: dependency", "someuser")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:459:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7f0a7861b7d0>, pr_labels = []
pr_title = 'fix: dependency', pr_author = 'someuser', author_team_slugs = None
def is_pr_eligible(
self,
pr_labels: list[str],
pr_title: str,
pr_author: str,
author_team_slugs: list[str] | None = None,
) -> tuple[bool, str | None]:
"""Check if a PR is eligible for auto-approval based on filters.
All configured filters must pass (AND logic between filter types).
Within each filter, any match is sufficient (OR logic).
Args:
pr_labels: List of label names on the PR
pr_title: PR title
pr_author: PR author's login
author_team_slugs: List of team slugs the author belongs to (for team filter)
Returns:
Tuple of (is_eligible, reason_if_not_eligible)
"""
# Check required labels filter
if self.required_labels:
if not any(label in self.required_labels for label in pr_labels):
return (
False,
f"PR missing required label (one of: {', '.join(self.required_labels)})",
)
# Check required title patterns filter
if self._compiled_title_patterns:
if not isinstance(pr_title, str):
return False, "PR title is not valid text"
if len(pr_title) > MAX_PR_TITLE_LENGTH:
return (
False,
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
for pattern in self._compiled_title_patterns:
try:
if pattern.search(
pr_title,
timeout=TITLE_PATTERN_TIMEOUT_SECONDS,
concurrent=True,
):
break
except TimeoutError:
timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
return (
False,
"PR title pattern evaluation could not complete within "
f"the {timeout_ms} ms safety limit",
)
> except regexCosmicRayTestingExceptionerror:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'regexCosmicRayTestingExceptionerror' is not defined
stampbot/config.py:333: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_regex_engine_failure_is_closed - NameError: name 'regexCosmicRayTestingExceptionerror' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 43 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 6, definition_name: from_toml
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -418,7 +418,7 @@
if toml_content:
try:
repo_settings = toml.loads(toml_content)
- except Exception as e:
+ except CosmicRayTestingException as e:
raise ValueError(f"Invalid TOML content: {e}") from e
# Merge: repo settings override defaults..............F
=================================== FAILURES ===================================
___________________ test_repo_config_from_toml_parser_error ____________________
cls = <class 'stampbot.config.RepoConfig'>, toml_content = ';\r'
@classmethod
def from_toml(cls, toml_content: str) -> RepoConfig:
"""Parse TOML content and merge with app defaults.
Uses dynaconf to properly merge repo-specific settings with
the app-level defaults, allowing repos to override only
the settings they need to change.
Args:
toml_content: Raw TOML content from repo's stampbot.toml
Returns:
RepoConfig with merged settings
"""
import toml
# Start with defaults
defaults = cls._get_defaults()
# Parse the repo-specific TOML
repo_settings: dict[str, Any] = {}
if toml_content:
try:
> repo_settings = toml.loads(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/config.py:420:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
s = ';\r', _dict = <class 'dict'>
decoder = <toml.decoder.TomlDecoder object at 0x7fa8e8977d90>
def loads(s, _dict=dict, decoder=None):
"""Parses string as toml
Args:
s: String to be parsed
_dict: (optional) Specifies the class of the returned toml dictionary
Returns:
Parsed toml file represented as a dictionary
Raises:
TypeError: When a non-string is passed
TomlDecodeError: Error while decoding toml
"""
implicitgroups = []
if decoder is None:
decoder = TomlDecoder(_dict)
retval = decoder.get_empty_table()
currentlevel = retval
if not isinstance(s, basestring):
raise TypeError("Expecting something like a string")
if not isinstance(s, unicode):
s = s.decode('utf8')
original = s
sl = list(s)
openarr = 0
openstring = False
openstrchar = ""
multilinestr = False
arrayoftables = False
beginline = True
keygroup = False
dottedkey = False
keyname = 0
key = ''
prev_key = ''
line_no = 1
for i, item in enumerate(sl):
> if item == '\r' and sl[i + 1] == '\n':
^^^^^^^^^
E IndexError: list index out of range
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/toml/decoder.py:207: IndexError
During handling of the above exception, another exception occurred:
def test_repo_config_from_toml_parser_error():
"""Test parser errors are normalized as config validation errors."""
with pytest.raises(ValueError, match="Invalid TOML content"):
> RepoConfig.from_toml(";\r")
tests/test_config.py:176:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
cls = <class 'stampbot.config.RepoConfig'>, toml_content = ';\r'
@classmethod
def from_toml(cls, toml_content: str) -> RepoConfig:
"""Parse TOML content and merge with app defaults.
Uses dynaconf to properly merge repo-specific settings with
the app-level defaults, allowing repos to override only
the settings they need to change.
Args:
toml_content: Raw TOML content from repo's stampbot.toml
Returns:
RepoConfig with merged settings
"""
import toml
# Start with defaults
defaults = cls._get_defaults()
# Parse the repo-specific TOML
repo_settings: dict[str, Any] = {}
if toml_content:
try:
repo_settings = toml.loads(toml_content)
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/config.py:421: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml_parser_error - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 14 passed, 3 deselected, 1 warning in 2.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 7, definition_name: default_or_config_error
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -474,6 +474,6 @@
"""
try:
return cls.default()
- except ValueError as e:
+ except CosmicRayTestingException as e:
return cls.fail_closed(f"Invalid service default configuration: {e}")
.....F
=================================== FAILURES ===================================
_ test_repo_config_default_or_config_error_fails_closed[approve_commands-approve-must be a list of strings] _
cls = <class 'stampbot.config.RepoConfig'>
@classmethod
def default_or_config_error(cls) -> RepoConfig:
"""Return configured defaults, failing closed if they are invalid.
Returns:
Configured defaults, or a built-in config carrying config_error.
"""
try:
> return cls.default()
^^^^^^^^^^^^^
stampbot/config.py:476:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:454: in default
return cls(
stampbot/config.py:156: in __init__
self.approve_commands = self._validate_command_list(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
commands = 'approve', setting = 'approve_commands'
@staticmethod
def _validate_command_list(commands: object, setting: str) -> list[str]:
"""Require one command setting to be a list containing only strings.
Args:
commands: Parsed application or repository setting.
setting: Setting name used in the validation error.
Returns:
A detached copy of the validated command list.
Raises:
ValueError: If the setting is not a list of strings.
"""
if not isinstance(commands, list):
> raise ValueError(f"{setting} must be a list of strings")
E ValueError: approve_commands must be a list of strings
stampbot/config.py:187: ValueError
During handling of the above exception, another exception occurred:
setting = 'approve_commands', value = 'approve'
message = 'must be a list of strings'
@pytest.mark.parametrize(
("setting", "value", "message"),
[
("approve_commands", "approve", "must be a list of strings"),
("unapprove_commands", ["unapprove", 1], "must contain only strings"),
(
"chatops_required_permission",
"owner",
"Invalid chatops_required_permission",
),
(
"chatops_required_permission",
["maintain"],
"Invalid chatops_required_permission",
),
],
)
def test_repo_config_default_or_config_error_fails_closed(setting, value, message):
"""Test invalid service defaults produce a non-authorizing config."""
service_defaults = MagicMock(spec=[setting])
setattr(service_defaults, setting, value)
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
> config = RepoConfig.default_or_config_error()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:107:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
cls = <class 'stampbot.config.RepoConfig'>
@classmethod
def default_or_config_error(cls) -> RepoConfig:
"""Return configured defaults, failing closed if they are invalid.
Returns:
Configured defaults, or a built-in config carrying config_error.
"""
try:
return cls.default()
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/config.py:477: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_default_or_config_error_fails_closed[approve_commands-approve-must be a list of strings] - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 5 passed, 3 deselected, 1 warning in 2.66s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 0, definition_name: None
--- mutation diff --- --- a/stampbot/config.py +++ b/stampbot/config.py @@ -13,7 +13,7 @@ # GitHub currently limits pull request titles to 256 characters. Keep a local # bound because webhook JSON is still untrusted input. Repository patterns are # bounded separately so a policy cannot multiply matching work without limit. -MAX_PR_TITLE_LENGTH = 256 +MAX_PR_TITLE_LENGTH = 257 MAX_TITLE_PATTERN_COUNT = 20 MAX_TITLE_PATTERN_LENGTH = 256 TITLE_PATTERN_TIMEOUT_SECONDS = 0.01
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 1, definition_name: None
--- mutation diff --- --- a/stampbot/config.py +++ b/stampbot/config.py @@ -13,7 +13,7 @@ # GitHub currently limits pull request titles to 256 characters. Keep a local # bound because webhook JSON is still untrusted input. Repository patterns are # bounded separately so a policy cannot multiply matching work without limit. -MAX_PR_TITLE_LENGTH = 256 +MAX_PR_TITLE_LENGTH = 255 MAX_TITLE_PATTERN_COUNT = 20 MAX_TITLE_PATTERN_LENGTH = 256 TITLE_PATTERN_TIMEOUT_SECONDS = 0.01
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.97s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 2, definition_name: None
--- mutation diff --- --- a/stampbot/config.py +++ b/stampbot/config.py @@ -14,7 +14,7 @@ # bound because webhook JSON is still untrusted input. Repository patterns are # bounded separately so a policy cannot multiply matching work without limit. MAX_PR_TITLE_LENGTH = 256 -MAX_TITLE_PATTERN_COUNT = 20 +MAX_TITLE_PATTERN_COUNT = 21 MAX_TITLE_PATTERN_LENGTH = 256 TITLE_PATTERN_TIMEOUT_SECONDS = 0.01
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 3, definition_name: None
--- mutation diff --- --- a/stampbot/config.py +++ b/stampbot/config.py @@ -14,7 +14,7 @@ # bound because webhook JSON is still untrusted input. Repository patterns are # bounded separately so a policy cannot multiply matching work without limit. MAX_PR_TITLE_LENGTH = 256 -MAX_TITLE_PATTERN_COUNT = 20 +MAX_TITLE_PATTERN_COUNT = 19 MAX_TITLE_PATTERN_LENGTH = 256 TITLE_PATTERN_TIMEOUT_SECONDS = 0.01
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 4, definition_name: None
--- mutation diff --- --- a/stampbot/config.py +++ b/stampbot/config.py @@ -15,7 +15,7 @@ # bounded separately so a policy cannot multiply matching work without limit. MAX_PR_TITLE_LENGTH = 256 MAX_TITLE_PATTERN_COUNT = 20 -MAX_TITLE_PATTERN_LENGTH = 256 +MAX_TITLE_PATTERN_LENGTH = 257 TITLE_PATTERN_TIMEOUT_SECONDS = 0.01 # Default values for repo configuration
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.12s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 5, definition_name: None
--- mutation diff --- --- a/stampbot/config.py +++ b/stampbot/config.py @@ -15,7 +15,7 @@ # bounded separately so a policy cannot multiply matching work without limit. MAX_PR_TITLE_LENGTH = 256 MAX_TITLE_PATTERN_COUNT = 20 -MAX_TITLE_PATTERN_LENGTH = 256 +MAX_TITLE_PATTERN_LENGTH = 255 TITLE_PATTERN_TIMEOUT_SECONDS = 0.01 # Default values for repo configuration
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 6, definition_name: None
--- mutation diff --- --- a/stampbot/config.py +++ b/stampbot/config.py @@ -16,7 +16,7 @@ MAX_PR_TITLE_LENGTH = 256 MAX_TITLE_PATTERN_COUNT = 20 MAX_TITLE_PATTERN_LENGTH = 256 -TITLE_PATTERN_TIMEOUT_SECONDS = 0.01 +TITLE_PATTERN_TIMEOUT_SECONDS = 1.01 # Default values for repo configuration # These can be overridden in settings.toml or per-repo stampbot.toml
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
...............................................F
=================================== FAILURES ===================================
________________ test_pathological_pr_title_pattern_is_bounded _________________
fut = <coroutine object WebhookHandler.handle_event at 0x7f5155156980>
timeout = 1.0
async def wait_for(fut, timeout):
"""Wait for the single Future or coroutine to complete, with timeout.
Coroutine will be wrapped in Task.
Returns result of the Future or coroutine. When a timeout occurs,
it cancels the task and raises TimeoutError. To avoid the task
cancellation, wrap it in shield().
If the wait is cancelled, the task is also cancelled.
If the task suppresses the cancellation and returns a value instead,
that value is returned.
This function is a coroutine.
"""
# The special case for timeout <= 0 is for the following case:
#
# async def test_waitfor():
# func_started = False
#
# async def func():
# nonlocal func_started
# func_started = True
#
# try:
# await asyncio.wait_for(func(), 0)
# except asyncio.TimeoutError:
# assert not func_started
# else:
# assert False
#
# asyncio.run(test_waitfor())
if timeout is not None and timeout <= 0:
fut = ensure_future(fut)
if fut.done():
return fut.result()
await _cancel_and_wait(fut)
try:
return fut.result()
except exceptions.CancelledError as exc:
raise TimeoutError from exc
async with timeouts.timeout(timeout):
> return await fut
^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/tasks.py:488:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/webhook_handler.py:164: in handle_event
result = await self._handle_pull_request(payload)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:279: in _handle_pull_request
result = await self._approve_for_label(span, event, label)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stampbot/webhook_handler.py:416: in _approve_for_label
is_eligible, reason = await run_in_threadpool(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/starlette/concurrency.py:34: in run_in_threadpool
return await anyio.to_thread.run_sync(func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/to_thread.py:63: in run_sync
return await get_async_backend().run_sync_in_worker_thread(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
cls = <class 'anyio._backends._asyncio.AsyncIOBackend'>
func = functools.partial(<bound method RepoConfig.is_pr_eligible of <stampbot.config.RepoConfig object at 0x7f51556dc890>>, [...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!', 'contributor', None)
args = (), abandon_on_cancel = False, limiter = None
@classmethod
async def run_sync_in_worker_thread( # type: ignore[return]
cls,
func: Callable[[Unpack[PosArgsT]], T_Retval],
args: tuple[Unpack[PosArgsT]],
abandon_on_cancel: bool = False,
limiter: abc.CapacityLimiter | None = None,
) -> T_Retval:
await cls.checkpoint()
# If this is the first run in this event loop thread, set up the necessary
# variables
try:
idle_workers = _threadpool_idle_workers.get()
workers = _threadpool_workers.get()
except LookupError:
idle_workers = deque()
workers = set()
_threadpool_idle_workers.set(idle_workers)
_threadpool_workers.set(workers)
async with limiter or cls.current_default_thread_limiter():
with CancelScope(shield=not abandon_on_cancel) as scope:
future = asyncio.Future[T_Retval]()
root_task = find_root_task()
if not idle_workers:
worker = WorkerThread(root_task, workers, idle_workers)
worker.start()
workers.add(worker)
root_task.add_done_callback(
worker.stop, context=contextvars.Context()
)
else:
worker = idle_workers.pop()
# Prune any other workers that have been idle for MAX_IDLE_TIME
# seconds or longer
now = cls.current_time()
while idle_workers:
if (
now - idle_workers[0].idle_since
< WorkerThread.MAX_IDLE_TIME
):
break
expired_worker = idle_workers.popleft()
expired_worker.root_task.remove_done_callback(
expired_worker.stop
)
expired_worker.stop()
context = copy_context()
context.run(set_current_async_library, None)
if abandon_on_cancel or scope._parent_scope is None:
worker_scope = scope
else:
worker_scope = scope._parent_scope
worker.queue.put_nowait((context, func, args, future, worker_scope))
> return await future
^^^^^^^^^^^^
E asyncio.exceptions.CancelledError
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:2596: CancelledError
The above exception was the direct cause of the following exception:
webhook_handler = <stampbot.webhook_handler.WebhookHandler object at 0x7f515529ff50>
mock_github_client = <MagicMock name='github_client' id='139987296138784'>
@pytest.mark.asyncio
async def test_pathological_pr_title_pattern_is_bounded(webhook_handler, mock_github_client):
"""Test a pathological title pattern fails closed within a bounded time."""
from stampbot.config import MAX_PR_TITLE_LENGTH
payload = load_fixture("pr_labeled_autoapprove")
payload["pull_request"]["title"] = "a" * (MAX_PR_TITLE_LENGTH - 1) + "!"
mock_github_client.get_repo_file.return_value = 'required_title_patterns = ["(a|aa)+$"]'
> result = await asyncio.wait_for(
webhook_handler.handle_event("pull_request", payload), timeout=1.0
)
tests/test_webhook_handler.py:766:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/tasks.py:487: in wait_for
async with timeouts.timeout(timeout):
^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Timeout [expired]>
exc_type = <class 'asyncio.exceptions.CancelledError'>
exc_val = CancelledError(), exc_tb = <traceback object at 0x7f51553c8bc0>
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> bool | None:
assert self._state in (_State.ENTERED, _State.EXPIRING)
if self._timeout_handler is not None:
self._timeout_handler.cancel()
self._timeout_handler = None
if self._state is _State.EXPIRING:
self._state = _State.EXPIRED
if self._task.uncancel() <= self._cancelling and exc_type is not None:
# Since there are no new cancel requests, we're
# handling this.
if issubclass(exc_type, exceptions.CancelledError):
> raise TimeoutError from exc_val
E TimeoutError
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/timeouts.py:115: TimeoutError
----------------------------- Captured stdout call -----------------------------
{"extra": {"event_type": "pull_request", "action": "labeled"}, "event": "Received pull_request event", "logger": "stampbot.webhook_handler", "level": "info", "timestamp": "2026-09-04T22:45:32.528975Z", "_logger": "<_FixedFindCallerLogger stampbot.webhook_handler (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.webhook_handler:webhook_handler.py:156 {'extra': {'event_type': 'pull_request', 'action': 'labeled'}, 'event': 'Received pull_request event', 'logger': 'stampbot.webhook_handler', 'level': 'info', 'timestamp': '2026-09-04T22:45:32.528975Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_webhook_handler.py::test_pathological_pr_title_pattern_is_bounded - TimeoutError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 407 passed, 3 deselected, 1 warning in 6.24s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 7, definition_name: None
--- mutation diff --- --- a/stampbot/config.py +++ b/stampbot/config.py @@ -16,7 +16,7 @@ MAX_PR_TITLE_LENGTH = 256 MAX_TITLE_PATTERN_COUNT = 20 MAX_TITLE_PATTERN_LENGTH = 256 -TITLE_PATTERN_TIMEOUT_SECONDS = 0.01 +TITLE_PATTERN_TIMEOUT_SECONDS = -0.99 # Default values for repo configuration # These can be overridden in settings.toml or per-repo stampbot.toml
timeout
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 8, definition_name: None
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -68,7 +68,7 @@
)
-DEFAULT_REPO_CONFIG_CACHE_SECONDS = 300
+DEFAULT_REPO_CONFIG_CACHE_SECONDS = 301
def repo_config_cache_seconds() -> int:..............................................................F
=================================== FAILURES ===================================
___________________ test_repo_config_cache_seconds_defaults ____________________
def test_repo_config_cache_seconds_defaults():
"""Test the cache lifetime defaults to five minutes."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.side_effect = lambda _key, default=None: default
> assert repo_config_cache_seconds() == 300
E assert 301 == 300
E + where 301 = repo_config_cache_seconds()
tests/test_config.py:635: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_defaults - assert 301 == 300
+ where 301 = repo_config_cache_seconds()
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 62 passed, 3 deselected, 1 warning in 2.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 9, definition_name: None
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -68,7 +68,7 @@
)
-DEFAULT_REPO_CONFIG_CACHE_SECONDS = 300
+DEFAULT_REPO_CONFIG_CACHE_SECONDS = 299
def repo_config_cache_seconds() -> int:..............................................................F
=================================== FAILURES ===================================
___________________ test_repo_config_cache_seconds_defaults ____________________
def test_repo_config_cache_seconds_defaults():
"""Test the cache lifetime defaults to five minutes."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.side_effect = lambda _key, default=None: default
> assert repo_config_cache_seconds() == 300
E assert 299 == 300
E + where 299 = repo_config_cache_seconds()
tests/test_config.py:635: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_defaults - assert 299 == 300
+ where 299 = repo_config_cache_seconds()
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 62 passed, 3 deselected, 1 warning in 2.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 10, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -90,7 +90,7 @@
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
- if seconds < 0:
+ if seconds < 1:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
return seconds
............................................................F
=================================== FAILURES ===================================
_____________ test_repo_config_cache_seconds_accepts_integers[0-0] _____________
raw = 0, expected = 0
@pytest.mark.parametrize(
("raw", "expected"),
[(300, 300), ("120", 120), (0, 0), (5.0, 5)],
)
def test_repo_config_cache_seconds_accepts_integers(raw, expected):
"""Test the cache lifetime accepts integer-valued settings."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = raw
> assert repo_config_cache_seconds() == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:627:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def repo_config_cache_seconds() -> int:
"""Return how long a repository's parsed policy stays cached, in seconds.
Returns:
A non-negative integer. Zero disables the cache.
Raises:
ValueError: If ``repo_config_cache_seconds`` is not a non-negative integer.
"""
raw = settings.get("repo_config_cache_seconds", DEFAULT_REPO_CONFIG_CACHE_SECONDS)
# Dynaconf parses environment values, so ``false`` arrives as a bool and
# ``1.9`` as a float. Neither is an integer setting, and int() would accept
# both silently.
if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}")
try:
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
if seconds < 1:
> raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
E ValueError: repo_config_cache_seconds must be 0 or greater, got 0
stampbot/config.py:94: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_accepts_integers[0-0] - ValueError: repo_config_cache_seconds must be 0 or greater, got 0
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 60 passed, 3 deselected, 1 warning in 2.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 11, definition_name: repo_config_cache_seconds
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -90,7 +90,7 @@
seconds = int(raw)
except (TypeError, ValueError) as error:
raise ValueError(f"repo_config_cache_seconds must be an integer, got {raw!r}") from error
- if seconds < 0:
+ if seconds < -1:
raise ValueError(f"repo_config_cache_seconds must be 0 or greater, got {raw!r}")
return seconds
..................................................................F
=================================== FAILURES ===================================
__________ test_repo_config_cache_seconds_rejects_invalid_values[-1] ___________
raw = '-1'
@pytest.mark.parametrize("raw", ["soon", None, -5, "-1", [300], True, False, 1.9, "1.9"])
def test_repo_config_cache_seconds_rejects_invalid_values(raw):
"""Test a non-integer or negative lifetime fails at startup instead of guessing."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = raw
> with pytest.raises(ValueError, match="repo_config_cache_seconds"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_config.py:644: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_cache_seconds_rejects_invalid_values[-1] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 66 passed, 3 deselected, 1 warning in 2.73s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 12, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1001)
return (
False,
"PR title pattern evaluation could not complete within "........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 13, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -324,7 +324,7 @@
):
break
except TimeoutError:
- timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)
+ timeout_ms = int(TITLE_PATTERN_TIMEOUT_SECONDS * 999)
return (
False,
"PR title pattern evaluation could not complete within "..........................................F
=================================== FAILURES ===================================
___________ test_is_pr_eligible_pathological_title_pattern_times_out ___________
def test_is_pr_eligible_pathological_title_pattern_times_out():
"""Test catastrophic backtracking is stopped by the match timeout."""
config = RepoConfig.from_toml('required_title_patterns = ["(a|aa)+$"]')
is_eligible, reason = config.is_pr_eligible(
[], "a" * (MAX_PR_TITLE_LENGTH - 1) + "!", "someuser"
)
assert is_eligible is False
> assert f"{int(TITLE_PATTERN_TIMEOUT_SECONDS * 1000)} ms safety limit" in reason
E AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 9 ms safety limit'
tests/test_config.py:450: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_pathological_title_pattern_times_out - AssertionError: assert '10 ms safety limit' in 'PR title pattern evaluation could not complete within the 9 ms safety limit'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 42 passed, 3 deselected, 1 warning in 2.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 14, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -345,7 +345,7 @@
if self.allowed_teams and author_team_slugs:
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
- team_slug = team.split("/")[-1] if "/" in team else team
+ team_slug = team.split("/")[- 2] if "/" in team else team
if team_slug in author_team_slugs:
return True, None
.................................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_team _______________________
def test_is_pr_eligible_allowed_team():
"""Test is_pr_eligible returns True when author is in an allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "team-member", author_team_slugs=["release-team"]
)
> assert is_eligible is True
E assert False is True
tests/test_config.py:523: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_team - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 49 passed, 3 deselected, 1 warning in 2.72s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 15, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -345,7 +345,7 @@
if self.allowed_teams and author_team_slugs:
for team in self.allowed_teams:
# Support both "org/team" and "team" formats
- team_slug = team.split("/")[-1] if "/" in team else team
+ team_slug = team.split("/")[- 0] if "/" in team else team
if team_slug in author_team_slugs:
return True, None
.................................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_team _______________________
def test_is_pr_eligible_allowed_team():
"""Test is_pr_eligible returns True when author is in an allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "team-member", author_team_slugs=["release-team"]
)
> assert is_eligible is True
E assert False is True
tests/test_config.py:523: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_team - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 49 passed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 0, definition_name: RepoConfig
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -168,8 +168,6 @@
self.allowed_users = allowed_users or []
self.allowed_teams = allowed_teams or []
self.config_error = config_error
-
- @staticmethod
def _validate_command_list(commands: object, setting: str) -> list[str]:
"""Require one command setting to be a list containing only strings.
F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:427: in from_toml
return cls(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7f87f0a3d2b0>
approval_labels = ['test', 'autoapprove'], auto_approve_on_label = True
reapprove = True, chatops_enabled = True, chatops_required_permission = 'write'
approve_commands = ['approve', 'stamp'], unapprove_commands = ['unapprove']
required_labels = [], required_title_patterns = [], allowed_users = []
allowed_teams = [], config_error = None
def __init__(
self,
approval_labels: list[str],
auto_approve_on_label: bool,
reapprove: bool,
chatops_enabled: bool,
chatops_required_permission: str,
approve_commands: list[str],
unapprove_commands: list[str],
required_labels: list[str] | None = None,
required_title_patterns: list[str] | None = None,
allowed_users: list[str] | None = None,
allowed_teams: list[str] | None = None,
config_error: str | None = None,
):
"""Initialize repo configuration.
Args:
approval_labels: Labels that trigger auto-approval
auto_approve_on_label: Whether to auto-approve when label is added
reapprove: Whether to re-approve when new commits are pushed
chatops_enabled: Whether chatops commands are enabled
chatops_required_permission: Minimum repo permission for chatops commands
approve_commands: Commands that trigger approval
unapprove_commands: Commands that dismiss approval
required_labels: Labels required for auto-approval eligibility (any match)
required_title_patterns: Regex patterns for PR title (any match)
allowed_users: User logins allowed for auto-approval (any match)
allowed_teams: Team slugs allowed for auto-approval (any match)
config_error: Config error message if config was invalid
"""
self.approval_labels = approval_labels
self.auto_approve_on_label = auto_approve_on_label
self.reapprove = reapprove
self.chatops_enabled = chatops_enabled
self.chatops_required_permission = self._validate_required_permission(
chatops_required_permission
)
> self.approve_commands = self._validate_command_list(
approve_commands,
"approve_commands",
)
E TypeError: RepoConfig._validate_command_list() takes 2 positional arguments but 3 were given
stampbot/config.py:156: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - TypeError: RepoConfig._validate_command_list() takes 2 positional arguments but 3 were given
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 1, definition_name: RepoConfig
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -188,8 +188,6 @@
if any(not isinstance(command, str) for command in commands):
raise ValueError(f"{setting} must contain only strings")
return list(commands)
-
- @staticmethod
def _validate_required_permission(permission: object) -> str:
"""Require the ChatOps permission threshold to be a supported value.
F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:427: in from_toml
return cls(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7fe7a64352b0>
approval_labels = ['test', 'autoapprove'], auto_approve_on_label = True
reapprove = True, chatops_enabled = True, chatops_required_permission = 'write'
approve_commands = ['approve', 'stamp'], unapprove_commands = ['unapprove']
required_labels = [], required_title_patterns = [], allowed_users = []
allowed_teams = [], config_error = None
def __init__(
self,
approval_labels: list[str],
auto_approve_on_label: bool,
reapprove: bool,
chatops_enabled: bool,
chatops_required_permission: str,
approve_commands: list[str],
unapprove_commands: list[str],
required_labels: list[str] | None = None,
required_title_patterns: list[str] | None = None,
allowed_users: list[str] | None = None,
allowed_teams: list[str] | None = None,
config_error: str | None = None,
):
"""Initialize repo configuration.
Args:
approval_labels: Labels that trigger auto-approval
auto_approve_on_label: Whether to auto-approve when label is added
reapprove: Whether to re-approve when new commits are pushed
chatops_enabled: Whether chatops commands are enabled
chatops_required_permission: Minimum repo permission for chatops commands
approve_commands: Commands that trigger approval
unapprove_commands: Commands that dismiss approval
required_labels: Labels required for auto-approval eligibility (any match)
required_title_patterns: Regex patterns for PR title (any match)
allowed_users: User logins allowed for auto-approval (any match)
allowed_teams: Team slugs allowed for auto-approval (any match)
config_error: Config error message if config was invalid
"""
self.approval_labels = approval_labels
self.auto_approve_on_label = auto_approve_on_label
self.reapprove = reapprove
self.chatops_enabled = chatops_enabled
> self.chatops_required_permission = self._validate_required_permission(
chatops_required_permission
)
E TypeError: RepoConfig._validate_required_permission() takes 1 positional argument but 2 were given
stampbot/config.py:153: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - TypeError: RepoConfig._validate_required_permission() takes 1 positional argument but 2 were given
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 2, definition_name: RepoConfig
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -209,8 +209,6 @@
f"Valid values: {', '.join(REPO_PERMISSION_LEVELS)}"
)
return permission
-
- @staticmethod
def _compile_title_patterns(patterns: list[str]) -> list[Any]:
"""Validate and compile bounded pull request title patterns.
F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/config.py:427: in from_toml
return cls(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <stampbot.config.RepoConfig object at 0x7f830ea5d2b0>
approval_labels = ['test', 'autoapprove'], auto_approve_on_label = True
reapprove = True, chatops_enabled = True, chatops_required_permission = 'write'
approve_commands = ['approve', 'stamp'], unapprove_commands = ['unapprove']
required_labels = [], required_title_patterns = [], allowed_users = []
allowed_teams = [], config_error = None
def __init__(
self,
approval_labels: list[str],
auto_approve_on_label: bool,
reapprove: bool,
chatops_enabled: bool,
chatops_required_permission: str,
approve_commands: list[str],
unapprove_commands: list[str],
required_labels: list[str] | None = None,
required_title_patterns: list[str] | None = None,
allowed_users: list[str] | None = None,
allowed_teams: list[str] | None = None,
config_error: str | None = None,
):
"""Initialize repo configuration.
Args:
approval_labels: Labels that trigger auto-approval
auto_approve_on_label: Whether to auto-approve when label is added
reapprove: Whether to re-approve when new commits are pushed
chatops_enabled: Whether chatops commands are enabled
chatops_required_permission: Minimum repo permission for chatops commands
approve_commands: Commands that trigger approval
unapprove_commands: Commands that dismiss approval
required_labels: Labels required for auto-approval eligibility (any match)
required_title_patterns: Regex patterns for PR title (any match)
allowed_users: User logins allowed for auto-approval (any match)
allowed_teams: Team slugs allowed for auto-approval (any match)
config_error: Config error message if config was invalid
"""
self.approval_labels = approval_labels
self.auto_approve_on_label = auto_approve_on_label
self.reapprove = reapprove
self.chatops_enabled = chatops_enabled
self.chatops_required_permission = self._validate_required_permission(
chatops_required_permission
)
self.approve_commands = self._validate_command_list(
approve_commands,
"approve_commands",
)
self.unapprove_commands = self._validate_command_list(
unapprove_commands,
"unapprove_commands",
)
self.required_labels = required_labels or []
title_patterns = required_title_patterns if required_title_patterns is not None else []
> self._compiled_title_patterns = self._compile_title_patterns(title_patterns)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: RepoConfig._compile_title_patterns() takes 1 positional argument but 2 were given
stampbot/config.py:166: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - TypeError: RepoConfig._compile_title_patterns() takes 1 positional argument but 2 were given
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 3, definition_name: RepoConfig
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -246,8 +246,6 @@
raise ValueError(f"Invalid regex pattern '{pattern}': {e}") from e
return compiled_patterns
-
- @classmethod
def fail_closed(cls, message: str) -> RepoConfig:
"""Return a valid config object that cannot authorize automation.
..F
=================================== FAILURES ===================================
_________ test_repo_config_fail_closed_does_not_read_service_defaults __________
def test_repo_config_fail_closed_does_not_read_service_defaults():
"""Test fail-closed config is independent of invalid service settings."""
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.side_effect = AssertionError("service settings must not be read")
> config = RepoConfig.fail_closed("invalid service configuration")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: RepoConfig.fail_closed() missing 1 required positional argument: 'message'
tests/test_config.py:59: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_fail_closed_does_not_read_service_defaults - TypeError: RepoConfig.fail_closed() missing 1 required positional argument: 'message'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 2 passed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 4, definition_name: RepoConfig
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -373,8 +373,6 @@
if self.allowed_users and pr_author in self.allowed_users:
return False
return True
-
- @classmethod
def _get_defaults(cls) -> dict[str, Any]:
"""Get default repo config values.
F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_config.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
cls = <class 'stampbot.config.RepoConfig'>
toml_content = '\napproval_labels = ["test", "autoapprove"]\nauto_approve_on_label = true\nreapprove = true\nchatops_enabled = true\nchatops_required_permission = "write"\napprove_commands = ["approve", "stamp"]\nunapprove_commands = ["unapprove"]\n'
@classmethod
def from_toml(cls, toml_content: str) -> RepoConfig:
"""Parse TOML content and merge with app defaults.
Uses dynaconf to properly merge repo-specific settings with
the app-level defaults, allowing repos to override only
the settings they need to change.
Args:
toml_content: Raw TOML content from repo's stampbot.toml
Returns:
RepoConfig with merged settings
"""
import toml
# Start with defaults
> defaults = cls._get_defaults()
^^^^^^^^^^^^^^^^^^^
E TypeError: RepoConfig._get_defaults() missing 1 required positional argument: 'cls'
stampbot/config.py:412: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - TypeError: RepoConfig._get_defaults() missing 1 required positional argument: 'cls'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 5, definition_name: RepoConfig
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -393,8 +393,6 @@
defaults[key] = getattr(settings_defaults, key)
return defaults
-
- @classmethod
def from_toml(cls, toml_content: str) -> RepoConfig:
"""Parse TOML content and merge with app defaults.
F
=================================== FAILURES ===================================
__________________________ test_repo_config_from_toml __________________________
def test_repo_config_from_toml():
"""Test parsing repository config from TOML."""
toml_content = """
approval_labels = ["test", "autoapprove"]
auto_approve_on_label = true
reapprove = true
chatops_enabled = true
chatops_required_permission = "write"
approve_commands = ["approve", "stamp"]
unapprove_commands = ["unapprove"]
"""
> config = RepoConfig.from_toml(toml_content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: RepoConfig.from_toml() missing 1 required positional argument: 'toml_content'
tests/test_config.py:31: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_from_toml - TypeError: RepoConfig.from_toml() missing 1 required positional argument: 'toml_content'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 3 deselected, 1 warning in 2.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 6, definition_name: RepoConfig
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -442,8 +442,6 @@
allowed_users=merged.get("allowed_users", []),
allowed_teams=merged.get("allowed_teams", []),
)
-
- @classmethod
def default(cls) -> RepoConfig:
"""Return default configuration from app settings.
.F
=================================== FAILURES ===================================
___________________________ test_repo_config_default ___________________________
def test_repo_config_default():
"""Test default repository config."""
> config = RepoConfig.default()
^^^^^^^^^^^^^^^^^^^^
E TypeError: RepoConfig.default() missing 1 required positional argument: 'cls'
tests/test_config.py:44: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_default - TypeError: RepoConfig.default() missing 1 required positional argument: 'cls'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 1 passed, 3 deselected, 1 warning in 2.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 7, definition_name: RepoConfig
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -464,8 +464,6 @@
allowed_users=defaults["allowed_users"],
allowed_teams=defaults["allowed_teams"],
)
-
- @classmethod
def default_or_config_error(cls) -> RepoConfig:
"""Return configured defaults, failing closed if they are invalid.
....F
=================================== FAILURES ===================================
______ test_repo_config_default_or_config_error_preserves_valid_defaults _______
def test_repo_config_default_or_config_error_preserves_valid_defaults():
"""Test valid configured defaults remain active."""
> config = RepoConfig.default_or_config_error()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E TypeError: RepoConfig.default_or_config_error() missing 1 required positional argument: 'cls'
tests/test_config.py:75: TypeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_default_or_config_error_preserves_valid_defaults - TypeError: RepoConfig.default_or_config_error() missing 1 required positional argument: 'cls'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 4 passed, 3 deselected, 1 warning in 2.72s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 0, definition_name: _compile_title_patterns
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -232,7 +232,7 @@
)
compiled_patterns = []
- for pattern in patterns:
+ for pattern in []:
if not isinstance(pattern, str):
raise ValueError("required_title_patterns must contain only strings")
if len(pattern) > MAX_TITLE_PATTERN_LENGTH:..........................F
=================================== FAILURES ===================================
____________________ test_repo_config_invalid_regex_pattern ____________________
def test_repo_config_invalid_regex_pattern():
"""Test invalid regex pattern raises a ValueError."""
toml_content = """
required_title_patterns = ["[invalid"]
"""
> with pytest.raises(ValueError, match="Invalid regex pattern"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_config.py:306: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_invalid_regex_pattern - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 26 passed, 3 deselected, 1 warning in 2.73s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 1, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -315,7 +315,7 @@
f"PR title exceeds the {MAX_PR_TITLE_LENGTH}-character safety limit",
)
- for pattern in self._compiled_title_patterns:
+ for pattern in []:
try:
if pattern.search(
pr_title,....................................F
=================================== FAILURES ===================================
__________________ test_is_pr_eligible_title_pattern_matches ___________________
def test_is_pr_eligible_title_pattern_matches():
"""Test is_pr_eligible returns True when title matches pattern."""
config = RepoConfig.from_toml('required_title_patterns = ["^chore:", "^\\\\[bot\\\\]"]')
is_eligible, reason = config.is_pr_eligible([], "chore: update dependencies", "someuser")
> assert is_eligible is True
E assert False is True
tests/test_config.py:386: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_title_pattern_matches - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 36 passed, 3 deselected, 1 warning in 2.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 2, definition_name: is_pr_eligible
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -343,7 +343,7 @@
# Or if they're a member of an allowed team
if self.allowed_teams and author_team_slugs:
- for team in self.allowed_teams:
+ for team in []:
# Support both "org/team" and "team" formats
team_slug = team.split("/")[-1] if "/" in team else team
if team_slug in author_team_slugs:.................................................F
=================================== FAILURES ===================================
_______________________ test_is_pr_eligible_allowed_team _______________________
def test_is_pr_eligible_allowed_team():
"""Test is_pr_eligible returns True when author is in an allowed team."""
config = RepoConfig.from_toml('allowed_teams = ["my-org/release-team"]')
is_eligible, reason = config.is_pr_eligible(
[], "Update deps", "team-member", author_team_slugs=["release-team"]
)
> assert is_eligible is True
E assert False is True
tests/test_config.py:523: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_is_pr_eligible_allowed_team - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 49 passed, 3 deselected, 1 warning in 2.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 3, definition_name: _get_defaults
--- mutation diff ---
--- a/stampbot/config.py
+++ b/stampbot/config.py
@@ -388,7 +388,7 @@
# Allow overriding defaults via settings.toml [defaults] section
if settings.get("defaults"):
settings_defaults = settings.defaults
- for key in defaults:
+ for key in []:
if hasattr(settings_defaults, key):
defaults[key] = getattr(settings_defaults, key)
.....F
=================================== FAILURES ===================================
_ test_repo_config_default_or_config_error_fails_closed[approve_commands-approve-must be a list of strings] _
setting = 'approve_commands', value = 'approve'
message = 'must be a list of strings'
@pytest.mark.parametrize(
("setting", "value", "message"),
[
("approve_commands", "approve", "must be a list of strings"),
("unapprove_commands", ["unapprove", 1], "must contain only strings"),
(
"chatops_required_permission",
"owner",
"Invalid chatops_required_permission",
),
(
"chatops_required_permission",
["maintain"],
"Invalid chatops_required_permission",
),
],
)
def test_repo_config_default_or_config_error_fails_closed(setting, value, message):
"""Test invalid service defaults produce a non-authorizing config."""
service_defaults = MagicMock(spec=[setting])
setattr(service_defaults, setting, value)
with patch("stampbot.config.settings") as mock_settings:
mock_settings.get.return_value = service_defaults
mock_settings.defaults = service_defaults
config = RepoConfig.default_or_config_error()
> assert config.config_error is not None
E assert None is not None
E + where None = <stampbot.config.RepoConfig object at 0x7f34bfcd3ad0>.config_error
tests/test_config.py:109: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_config.py::test_repo_config_default_or_config_error_fails_closed[approve_commands-approve-must be a list of strings] - assert None is not None
+ where None = <stampbot.config.RepoConfig object at 0x7f34bfcd3ad0>.config_error
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 5 passed, 3 deselected, 1 warning in 2.73s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -74,7 +74,7 @@
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
- endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
+ endpoint_uses_https = urlparse(settings.otel_endpoint).scheme != "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................F
=================================== FAILURES ===================================
______________ test_configure_telemetry_plaintext_requires_opt_in ______________
def test_configure_telemetry_plaintext_requires_opt_in():
"""Test plaintext OTLP is used only when explicitly enabled."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource"),
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor"),
patch("stampbot.telemetry.trace"),
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://otel-collector:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = True
mock_provider_cls.return_value = Mock()
from stampbot.telemetry import configure_telemetry
configure_telemetry()
> mock_exporter.assert_called_once_with(
endpoint="http://otel-collector:4317",
insecure=True,
)
tests/test_telemetry.py:239:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140350372324960'>, args = ()
kwargs = {'endpoint': 'http://otel-collector:4317', 'insecure': True}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
E Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Ignoring plaintext OTLP setting for an HTTPS endpoint", "logger": "stampbot.telemetry", "level": "warning", "timestamp": "2026-09-04T20:19:37.014104Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "warning"}
{"extra": {"otel_endpoint": "http://otel-collector:4317", "otel_transport": "TLS"}, "event": "OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T20:19:37.014834Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.telemetry:telemetry.py:79 {'event': 'Ignoring plaintext OTLP setting for an HTTPS endpoint', 'logger': 'stampbot.telemetry', 'level': 'warning', 'timestamp': '2026-09-04T20:19:37.014104Z'}
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://otel-collector:4317', 'otel_transport': 'TLS'}, 'event': 'OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T20:19:37.014834Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_plaintext_requires_opt_in - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 330 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -74,7 +74,7 @@
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
- endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
+ endpoint_uses_https = urlparse(settings.otel_endpoint).scheme < "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................F
=================================== FAILURES ===================================
______________ test_configure_telemetry_plaintext_requires_opt_in ______________
def test_configure_telemetry_plaintext_requires_opt_in():
"""Test plaintext OTLP is used only when explicitly enabled."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource"),
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor"),
patch("stampbot.telemetry.trace"),
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://otel-collector:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = True
mock_provider_cls.return_value = Mock()
from stampbot.telemetry import configure_telemetry
configure_telemetry()
> mock_exporter.assert_called_once_with(
endpoint="http://otel-collector:4317",
insecure=True,
)
tests/test_telemetry.py:239:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140669132023392'>, args = ()
kwargs = {'endpoint': 'http://otel-collector:4317', 'insecure': True}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
E Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Ignoring plaintext OTLP setting for an HTTPS endpoint", "logger": "stampbot.telemetry", "level": "warning", "timestamp": "2026-09-04T21:51:21.936700Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "warning"}
{"extra": {"otel_endpoint": "http://otel-collector:4317", "otel_transport": "TLS"}, "event": "OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T21:51:21.937412Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.telemetry:telemetry.py:79 {'event': 'Ignoring plaintext OTLP setting for an HTTPS endpoint', 'logger': 'stampbot.telemetry', 'level': 'warning', 'timestamp': '2026-09-04T21:51:21.936700Z'}
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://otel-collector:4317', 'otel_transport': 'TLS'}, 'event': 'OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T21:51:21.937412Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_plaintext_requires_opt_in - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 330 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -74,7 +74,7 @@
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
- endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
+ endpoint_uses_https = urlparse(settings.otel_endpoint).scheme <= "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................F
=================================== FAILURES ===================================
______________ test_configure_telemetry_plaintext_requires_opt_in ______________
def test_configure_telemetry_plaintext_requires_opt_in():
"""Test plaintext OTLP is used only when explicitly enabled."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource"),
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor"),
patch("stampbot.telemetry.trace"),
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://otel-collector:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = True
mock_provider_cls.return_value = Mock()
from stampbot.telemetry import configure_telemetry
configure_telemetry()
> mock_exporter.assert_called_once_with(
endpoint="http://otel-collector:4317",
insecure=True,
)
tests/test_telemetry.py:239:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140088178959968'>, args = ()
kwargs = {'endpoint': 'http://otel-collector:4317', 'insecure': True}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
E Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Ignoring plaintext OTLP setting for an HTTPS endpoint", "logger": "stampbot.telemetry", "level": "warning", "timestamp": "2026-09-04T21:28:04.997410Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "warning"}
{"extra": {"otel_endpoint": "http://otel-collector:4317", "otel_transport": "TLS"}, "event": "OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T21:28:04.998144Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.telemetry:telemetry.py:79 {'event': 'Ignoring plaintext OTLP setting for an HTTPS endpoint', 'logger': 'stampbot.telemetry', 'level': 'warning', 'timestamp': '2026-09-04T21:28:04.997410Z'}
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://otel-collector:4317', 'otel_transport': 'TLS'}, 'event': 'OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T21:28:04.998144Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_plaintext_requires_opt_in - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 330 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -74,7 +74,7 @@
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
- endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
+ endpoint_uses_https = urlparse(settings.otel_endpoint).scheme > "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................F
=================================== FAILURES ===================================
_________ test_configure_telemetry_https_endpoint_cannot_be_downgraded _________
def test_configure_telemetry_https_endpoint_cannot_be_downgraded():
"""Test an HTTPS endpoint keeps TLS when plaintext is requested."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource"),
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor"),
patch("stampbot.telemetry.trace"),
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "https://otel-collector:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = True
mock_provider_cls.return_value = Mock()
from stampbot.telemetry import configure_telemetry
configure_telemetry()
> mock_exporter.assert_called_once_with(
endpoint="https://otel-collector:4317",
insecure=False,
)
tests/test_telemetry.py:291:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140453823786896'>, args = ()
kwargs = {'endpoint': 'https://otel-collector:4317', 'insecure': False}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='https://otel-collector:4317', insecure=False)
E Actual: OTLPSpanExporter(endpoint='https://otel-collector:4317', insecure=True)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "https://otel-collector:4317", "otel_transport": "plaintext"}, "event": "OpenTelemetry configured with endpoint: https://otel-collector:4317 (plaintext)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T20:44:42.744866Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'https://otel-collector:4317', 'otel_transport': 'plaintext'}, 'event': 'OpenTelemetry configured with endpoint: https://otel-collector:4317 (plaintext)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T20:44:42.744866Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_https_endpoint_cannot_be_downgraded - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='https://otel-collector:4317', insecure=False)
Actual: OTLPSpanExporter(endpoint='https://otel-collector:4317', insecure=True)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 332 passed, 3 deselected, 1 warning in 4.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -74,7 +74,7 @@
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
- endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
+ endpoint_uses_https = urlparse(settings.otel_endpoint).scheme >= "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -74,7 +74,7 @@
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
- endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
+ endpoint_uses_https = urlparse(settings.otel_endpoint).scheme is "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
............................................F
=================================== FAILURES ===================================
_________ test_configure_telemetry_https_endpoint_cannot_be_downgraded _________
def test_configure_telemetry_https_endpoint_cannot_be_downgraded():
"""Test an HTTPS endpoint keeps TLS when plaintext is requested."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource"),
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor"),
patch("stampbot.telemetry.trace"),
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "https://otel-collector:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = True
mock_provider_cls.return_value = Mock()
from stampbot.telemetry import configure_telemetry
configure_telemetry()
> mock_exporter.assert_called_once_with(
endpoint="https://otel-collector:4317",
insecure=False,
)
tests/test_telemetry.py:291:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140500471738256'>, args = ()
kwargs = {'endpoint': 'https://otel-collector:4317', 'insecure': False}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='https://otel-collector:4317', insecure=False)
E Actual: OTLPSpanExporter(endpoint='https://otel-collector:4317', insecure=True)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "https://otel-collector:4317", "otel_transport": "plaintext"}, "event": "OpenTelemetry configured with endpoint: https://otel-collector:4317 (plaintext)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T22:24:32.095885Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'https://otel-collector:4317', 'otel_transport': 'plaintext'}, 'event': 'OpenTelemetry configured with endpoint: https://otel-collector:4317 (plaintext)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T22:24:32.095885Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/telemetry.py:77
/home/runner/work/stampbot/stampbot/stampbot/telemetry.py:77: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme is "https"
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_https_endpoint_cannot_be_downgraded - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='https://otel-collector:4317', insecure=False)
Actual: OTLPSpanExporter(endpoint='https://otel-collector:4317', insecure=True)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 332 passed, 3 deselected, 2 warnings in 4.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -74,7 +74,7 @@
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
- endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
+ endpoint_uses_https = urlparse(settings.otel_endpoint).scheme is not "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................F
=================================== FAILURES ===================================
______________ test_configure_telemetry_plaintext_requires_opt_in ______________
def test_configure_telemetry_plaintext_requires_opt_in():
"""Test plaintext OTLP is used only when explicitly enabled."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource"),
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor"),
patch("stampbot.telemetry.trace"),
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://otel-collector:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = True
mock_provider_cls.return_value = Mock()
from stampbot.telemetry import configure_telemetry
configure_telemetry()
> mock_exporter.assert_called_once_with(
endpoint="http://otel-collector:4317",
insecure=True,
)
tests/test_telemetry.py:239:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140514502731360'>, args = ()
kwargs = {'endpoint': 'http://otel-collector:4317', 'insecure': True}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
E Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Ignoring plaintext OTLP setting for an HTTPS endpoint", "logger": "stampbot.telemetry", "level": "warning", "timestamp": "2026-09-04T22:49:08.119412Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "warning"}
{"extra": {"otel_endpoint": "http://otel-collector:4317", "otel_transport": "TLS"}, "event": "OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T22:49:08.120135Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.telemetry:telemetry.py:79 {'event': 'Ignoring plaintext OTLP setting for an HTTPS endpoint', 'logger': 'stampbot.telemetry', 'level': 'warning', 'timestamp': '2026-09-04T22:49:08.119412Z'}
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://otel-collector:4317', 'otel_transport': 'TLS'}, 'event': 'OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T22:49:08.120135Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/telemetry.py:77
/home/runner/work/stampbot/stampbot/stampbot/telemetry.py:77: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme is not "https"
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_plaintext_requires_opt_in - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 330 passed, 3 deselected, 2 warnings in 3.97s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Is_Eq, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -73,7 +73,7 @@
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
- otel_insecure_requested = settings.get("otel_insecure", False) is True
+ otel_insecure_requested = settings.get("otel_insecure", False) == True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_NotEq, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -73,7 +73,7 @@
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
- otel_insecure_requested = settings.get("otel_insecure", False) is True
+ otel_insecure_requested = settings.get("otel_insecure", False) != True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.......................................F
=================================== FAILURES ===================================
_______________________ test_configure_telemetry_success _______________________
def test_configure_telemetry_success():
"""Test configure_telemetry returns provider when properly configured."""
from stampbot.version import APP_VERSION
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource") as mock_resource,
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor") as mock_processor,
patch("stampbot.telemetry.trace") as mock_trace,
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://localhost:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = False
mock_provider = Mock()
mock_provider_cls.return_value = mock_provider
from stampbot.telemetry import configure_telemetry
result = configure_telemetry()
assert result == mock_provider
mock_resource.create.assert_called_once_with(
{"service.name": "test-service", "service.version": APP_VERSION}
)
mock_provider_cls.assert_called_once()
> mock_exporter.assert_called_once_with(
endpoint="http://localhost:4317",
insecure=False,
)
tests/test_telemetry.py:168:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='139895596694368'>, args = ()
kwargs = {'endpoint': 'http://localhost:4317', 'insecure': False}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
E Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "http://localhost:4317", "otel_transport": "plaintext"}, "event": "OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T20:59:30.903258Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://localhost:4317', 'otel_transport': 'plaintext'}, 'event': 'OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T20:59:30.903258Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_success - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 327 passed, 3 deselected, 1 warning in 4.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Lt, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -73,7 +73,7 @@
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
- otel_insecure_requested = settings.get("otel_insecure", False) is True
+ otel_insecure_requested = settings.get("otel_insecure", False) < True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.......................................F
=================================== FAILURES ===================================
_______________________ test_configure_telemetry_success _______________________
def test_configure_telemetry_success():
"""Test configure_telemetry returns provider when properly configured."""
from stampbot.version import APP_VERSION
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource") as mock_resource,
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor") as mock_processor,
patch("stampbot.telemetry.trace") as mock_trace,
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://localhost:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = False
mock_provider = Mock()
mock_provider_cls.return_value = mock_provider
from stampbot.telemetry import configure_telemetry
result = configure_telemetry()
assert result == mock_provider
mock_resource.create.assert_called_once_with(
{"service.name": "test-service", "service.version": APP_VERSION}
)
mock_provider_cls.assert_called_once()
> mock_exporter.assert_called_once_with(
endpoint="http://localhost:4317",
insecure=False,
)
tests/test_telemetry.py:168:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='139852504234848'>, args = ()
kwargs = {'endpoint': 'http://localhost:4317', 'insecure': False}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
E Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "http://localhost:4317", "otel_transport": "plaintext"}, "event": "OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T21:07:38.840855Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://localhost:4317', 'otel_transport': 'plaintext'}, 'event': 'OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T21:07:38.840855Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_success - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 327 passed, 3 deselected, 1 warning in 4.14s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_LtE, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -73,7 +73,7 @@
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
- otel_insecure_requested = settings.get("otel_insecure", False) is True
+ otel_insecure_requested = settings.get("otel_insecure", False) <= True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.......................................F
=================================== FAILURES ===================================
_______________________ test_configure_telemetry_success _______________________
def test_configure_telemetry_success():
"""Test configure_telemetry returns provider when properly configured."""
from stampbot.version import APP_VERSION
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource") as mock_resource,
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor") as mock_processor,
patch("stampbot.telemetry.trace") as mock_trace,
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://localhost:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = False
mock_provider = Mock()
mock_provider_cls.return_value = mock_provider
from stampbot.telemetry import configure_telemetry
result = configure_telemetry()
assert result == mock_provider
mock_resource.create.assert_called_once_with(
{"service.name": "test-service", "service.version": APP_VERSION}
)
mock_provider_cls.assert_called_once()
> mock_exporter.assert_called_once_with(
endpoint="http://localhost:4317",
insecure=False,
)
tests/test_telemetry.py:168:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140290419080032'>, args = ()
kwargs = {'endpoint': 'http://localhost:4317', 'insecure': False}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
E Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "http://localhost:4317", "otel_transport": "plaintext"}, "event": "OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T21:47:44.643622Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://localhost:4317', 'otel_transport': 'plaintext'}, 'event': 'OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T21:47:44.643622Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_success - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 327 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_Gt, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -73,7 +73,7 @@
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
- otel_insecure_requested = settings.get("otel_insecure", False) is True
+ otel_insecure_requested = settings.get("otel_insecure", False) > True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................F
=================================== FAILURES ===================================
______________ test_configure_telemetry_plaintext_requires_opt_in ______________
def test_configure_telemetry_plaintext_requires_opt_in():
"""Test plaintext OTLP is used only when explicitly enabled."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource"),
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor"),
patch("stampbot.telemetry.trace"),
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://otel-collector:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = True
mock_provider_cls.return_value = Mock()
from stampbot.telemetry import configure_telemetry
configure_telemetry()
> mock_exporter.assert_called_once_with(
endpoint="http://otel-collector:4317",
insecure=True,
)
tests/test_telemetry.py:239:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='139964568594016'>, args = ()
kwargs = {'endpoint': 'http://otel-collector:4317', 'insecure': True}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
E Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "http://otel-collector:4317", "otel_transport": "TLS"}, "event": "OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T21:20:52.608410Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://otel-collector:4317', 'otel_transport': 'TLS'}, 'event': 'OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T21:20:52.608410Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_plaintext_requires_opt_in - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 330 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_GtE, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -73,7 +73,7 @@
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
- otel_insecure_requested = settings.get("otel_insecure", False) is True
+ otel_insecure_requested = settings.get("otel_insecure", False) >= True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...........................................F
=================================== FAILURES ===================================
________ test_configure_telemetry_rejects_non_boolean_plaintext_setting ________
def test_configure_telemetry_rejects_non_boolean_plaintext_setting():
"""Test a malformed insecure setting cannot disable TLS."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource"),
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor"),
patch("stampbot.telemetry.trace"),
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://otel-collector:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = "true"
mock_provider_cls.return_value = Mock()
from stampbot.telemetry import configure_telemetry
configure_telemetry()
> mock_exporter.assert_called_once_with(
endpoint="http://otel-collector:4317",
insecure=False,
)
tests/test_telemetry.py:265:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140413177345904'>, args = ()
kwargs = {'endpoint': 'http://otel-collector:4317', 'insecure': False}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'OTLPSpanExporter' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"error": "'>=' not supported between instances of 'str' and 'bool'"}, "event": "Failed to configure OpenTelemetry: '>=' not supported between instances of 'str' and 'bool'", "logger": "stampbot.telemetry", "level": "error", "timestamp": "2026-09-04T22:24:38.148372Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "error"}
------------------------------ Captured log call -------------------------------
ERROR stampbot.telemetry:telemetry.py:111 {'extra': {'error': "'>=' not supported between instances of 'str' and 'bool'"}, 'event': "Failed to configure OpenTelemetry: '>=' not supported between instances of 'str' and 'bool'", 'logger': 'stampbot.telemetry', 'level': 'error', 'timestamp': '2026-09-04T22:24:38.148372Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_rejects_non_boolean_plaintext_setting - AssertionError: Expected 'OTLPSpanExporter' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 331 passed, 3 deselected, 1 warning in 3.96s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -73,7 +73,7 @@
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
- otel_insecure_requested = settings.get("otel_insecure", False) is True
+ otel_insecure_requested = settings.get("otel_insecure", False) is not True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.......................................F
=================================== FAILURES ===================================
_______________________ test_configure_telemetry_success _______________________
def test_configure_telemetry_success():
"""Test configure_telemetry returns provider when properly configured."""
from stampbot.version import APP_VERSION
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource") as mock_resource,
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor") as mock_processor,
patch("stampbot.telemetry.trace") as mock_trace,
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://localhost:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = False
mock_provider = Mock()
mock_provider_cls.return_value = mock_provider
from stampbot.telemetry import configure_telemetry
result = configure_telemetry()
assert result == mock_provider
mock_resource.create.assert_called_once_with(
{"service.name": "test-service", "service.version": APP_VERSION}
)
mock_provider_cls.assert_called_once()
> mock_exporter.assert_called_once_with(
endpoint="http://localhost:4317",
insecure=False,
)
tests/test_telemetry.py:168:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140106644634464'>, args = ()
kwargs = {'endpoint': 'http://localhost:4317', 'insecure': False}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
E Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "http://localhost:4317", "otel_transport": "plaintext"}, "event": "OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T21:07:56.915994Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://localhost:4317', 'otel_transport': 'plaintext'}, 'event': 'OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T21:07:56.915994Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_success - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 327 passed, 3 deselected, 1 warning in 4.12s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 0, definition_name: configure_telemetry
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -36,7 +36,7 @@
Returns:
TracerProvider if telemetry is enabled, None otherwise
"""
- if not settings.otel_enabled:
+ if settings.otel_enabled:
logger.info("OpenTelemetry disabled")
return None
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:55: in <module>
configure_telemetry()
stampbot/telemetry.py:43: in configure_telemetry
provider = _configure_exporter()
^^^^^^^^^^^^^^^^^^^^^
stampbot/telemetry.py:59: in _configure_exporter
if not settings.otel_endpoint:
^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/dynaconf/base.py:98: in __getattr__
return getattr(self._wrapped, name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/dynaconf/base.py:372: in __getattr__
raise AttributeError(key)
E AttributeError: otel_endpoint
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - AttributeError: otel_endpoint
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.30s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 1, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -56,7 +56,7 @@
Returns:
TracerProvider when an endpoint is configured and setup succeeds, else None.
"""
- if not settings.otel_endpoint:
+ if settings.otel_endpoint:
logger.warning("OpenTelemetry enabled but no endpoint configured")
return None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.....................................F
=================================== FAILURES ===================================
_____________________ test_configure_telemetry_no_endpoint _____________________
library_instrumentors = (<MagicMock name='RequestsInstrumentor' id='140445887770512'>, <MagicMock name='LoggingInstrumentor' id='140445887254544'>)
def test_configure_telemetry_no_endpoint(library_instrumentors):
"""Test a missing endpoint exports nothing but still shapes log records."""
requests_instrumentor, logging_instrumentor = library_instrumentors
with patch("stampbot.telemetry.settings") as mock_settings:
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = None
from stampbot.telemetry import configure_telemetry
result = configure_telemetry()
> assert result is None
E assert <opentelemetry.sdk.trace.TracerProvider object at 0x7fbc1b385be0> is None
tests/test_telemetry.py:122: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid type MagicMock for attribute 'service.name' value. Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or a sequence of those types", "logger": "opentelemetry.attributes", "level": "warning", "timestamp": "2026-09-04T21:35:26.579186Z"}
{"extra": {"otel_endpoint": null, "otel_transport": "TLS"}, "event": "OpenTelemetry configured with endpoint: None (TLS)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T21:35:26.605097Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
WARNING opentelemetry.attributes:__init__.py:125 Invalid type MagicMock for attribute 'service.name' value. Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or a sequence of those types
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': None, 'otel_transport': 'TLS'}, 'event': 'OpenTelemetry configured with endpoint: None (TLS)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T21:35:26.605097Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_no_endpoint - assert <opentelemetry.sdk.trace.TracerProvider object at 0x7fbc1b385be0> is None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 325 passed, 3 deselected, 1 warning in 4.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 2, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -77,7 +77,7 @@
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
- otel_insecure = otel_insecure_requested and not endpoint_uses_https
+ otel_insecure = otel_insecure_requested and endpoint_uses_https
otlp_exporter = OTLPSpanExporter(
endpoint=settings.otel_endpoint,
insecure=otel_insecure,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..........................................F
=================================== FAILURES ===================================
______________ test_configure_telemetry_plaintext_requires_opt_in ______________
def test_configure_telemetry_plaintext_requires_opt_in():
"""Test plaintext OTLP is used only when explicitly enabled."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource"),
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor"),
patch("stampbot.telemetry.trace"),
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://otel-collector:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = True
mock_provider_cls.return_value = Mock()
from stampbot.telemetry import configure_telemetry
configure_telemetry()
> mock_exporter.assert_called_once_with(
endpoint="http://otel-collector:4317",
insecure=True,
)
tests/test_telemetry.py:239:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140230791603808'>, args = ()
kwargs = {'endpoint': 'http://otel-collector:4317', 'insecure': True}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
E Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "http://otel-collector:4317", "otel_transport": "TLS"}, "event": "OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T21:30:31.424102Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://otel-collector:4317', 'otel_transport': 'TLS'}, 'event': 'OpenTelemetry configured with endpoint: http://otel-collector:4317 (TLS)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T21:30:31.424102Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_plaintext_requires_opt_in - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=True)
Actual: OTLPSpanExporter(endpoint='http://otel-collector:4317', insecure=False)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 330 passed, 3 deselected, 1 warning in 4.09s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 3, definition_name: create_span
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -164,7 +164,7 @@
if span:
span.set_attribute("result", "success")
"""
- if not settings.otel_enabled:
+ if settings.otel_enabled:
yield None
return
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...................................F
=================================== FAILURES ===================================
_____________________ test_create_span_when_otel_disabled ______________________
def test_create_span_when_otel_disabled():
"""Test create_span yields None when OTEL is disabled."""
with patch("stampbot.telemetry.settings") as mock_settings:
mock_settings.otel_enabled = False
from stampbot.telemetry import create_span
with create_span("test_span", {"attr": "value"}) as span:
> assert span is None
E assert NonRecordingSpan(SpanContext(trace_id=0x00000000000000000000000000000000, span_id=0x0000000000000000, trace_flags=0x00, trace_state=[], is_remote=False)) is None
tests/test_telemetry.py:99: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_create_span_when_otel_disabled - assert NonRecordingSpan(SpanContext(trace_id=0x00000000000000000000000000000000, span_id=0x0000000000000000, trace_flags=0x00, trace_state=[], is_remote=False)) is None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 323 passed, 3 deselected, 1 warning in 3.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 0, definition_name: configure_telemetry
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -36,7 +36,7 @@
Returns:
TracerProvider if telemetry is enabled, None otherwise
"""
- if not settings.otel_enabled:
+ if not not settings.otel_enabled:
logger.info("OpenTelemetry disabled")
return None
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:55: in <module>
configure_telemetry()
stampbot/telemetry.py:43: in configure_telemetry
provider = _configure_exporter()
^^^^^^^^^^^^^^^^^^^^^
stampbot/telemetry.py:59: in _configure_exporter
if not settings.otel_endpoint:
^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/dynaconf/base.py:98: in __getattr__
return getattr(self._wrapped, name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/dynaconf/base.py:372: in __getattr__
raise AttributeError(key)
E AttributeError: otel_endpoint
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - AttributeError: otel_endpoint
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 2.25s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 1, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -56,7 +56,7 @@
Returns:
TracerProvider when an endpoint is configured and setup succeeds, else None.
"""
- if not settings.otel_endpoint:
+ if not not settings.otel_endpoint:
logger.warning("OpenTelemetry enabled but no endpoint configured")
return None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.....................................F
=================================== FAILURES ===================================
_____________________ test_configure_telemetry_no_endpoint _____________________
library_instrumentors = (<MagicMock name='RequestsInstrumentor' id='140046442475408'>, <MagicMock name='LoggingInstrumentor' id='140046440632336'>)
def test_configure_telemetry_no_endpoint(library_instrumentors):
"""Test a missing endpoint exports nothing but still shapes log records."""
requests_instrumentor, logging_instrumentor = library_instrumentors
with patch("stampbot.telemetry.settings") as mock_settings:
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = None
from stampbot.telemetry import configure_telemetry
result = configure_telemetry()
> assert result is None
E assert <opentelemetry.sdk.trace.TracerProvider object at 0x7f5f1a589be0> is None
tests/test_telemetry.py:122: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "Invalid type MagicMock for attribute 'service.name' value. Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or a sequence of those types", "logger": "opentelemetry.attributes", "level": "warning", "timestamp": "2026-09-04T21:34:54.657242Z"}
{"extra": {"otel_endpoint": null, "otel_transport": "TLS"}, "event": "OpenTelemetry configured with endpoint: None (TLS)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T21:34:54.683051Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
WARNING opentelemetry.attributes:__init__.py:125 Invalid type MagicMock for attribute 'service.name' value. Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or a sequence of those types
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': None, 'otel_transport': 'TLS'}, 'event': 'OpenTelemetry configured with endpoint: None (TLS)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T21:34:54.683051Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_no_endpoint - assert <opentelemetry.sdk.trace.TracerProvider object at 0x7f5f1a589be0> is None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 325 passed, 3 deselected, 1 warning in 4.18s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 2, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -75,7 +75,7 @@
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
- if otel_insecure_requested and endpoint_uses_https:
+ if not otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https
otlp_exporter = OTLPSpanExporter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.04s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 3, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -98,7 +98,7 @@
logger.info(
"OpenTelemetry configured with endpoint: %s (%s)",
settings.otel_endpoint,
- "plaintext" if otel_insecure else "TLS",
+ "plaintext" if not otel_insecure else "TLS",
extra={
"otel_endpoint": settings.otel_endpoint,
"otel_transport": "plaintext" if otel_insecure else "TLS",........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.01s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/AddNot, occurrence: 4, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -101,7 +101,7 @@
"plaintext" if otel_insecure else "TLS",
extra={
"otel_endpoint": settings.otel_endpoint,
- "otel_transport": "plaintext" if otel_insecure else "TLS",
+ "otel_transport": "plaintext" if not otel_insecure else "TLS",
},
)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 5, definition_name: instrument_fastapi
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -118,7 +118,7 @@
Args:
app: FastAPI application instance
"""
- if settings.otel_enabled:
+ if not settings.otel_enabled:
try:
FastAPIInstrumentor.instrument_app(app, excluded_urls=TRACE_EXCLUDED_URLS)
logger.info("FastAPI instrumented with OpenTelemetry")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..............................................F
=================================== FAILURES ===================================
_______________________ test_instrument_fastapi_success ________________________
def test_instrument_fastapi_success():
"""Test instrument_fastapi instruments app when OTEL is enabled."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.FastAPIInstrumentor") as mock_instrumentor,
):
mock_settings.otel_enabled = True
from stampbot.telemetry import instrument_fastapi
mock_app = Mock()
instrument_fastapi(mock_app)
# Probe endpoints are excluded so trace search shows webhook deliveries.
> mock_instrumentor.instrument_app.assert_called_once_with(
mock_app, excluded_urls="/health$,/ready$"
)
tests/test_telemetry.py:327:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='FastAPIInstrumentor.instrument_app' id='139672717276032'>
args = (<Mock id='139672717266288'>,)
kwargs = {'excluded_urls': '/health$,/ready$'}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'instrument_app' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_instrument_fastapi_success - AssertionError: Expected 'instrument_app' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 334 passed, 3 deselected, 1 warning in 4.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 6, definition_name: create_span
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -164,7 +164,7 @@
if span:
span.set_attribute("result", "success")
"""
- if not settings.otel_enabled:
+ if not not settings.otel_enabled:
yield None
return
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...................................F
=================================== FAILURES ===================================
_____________________ test_create_span_when_otel_disabled ______________________
def test_create_span_when_otel_disabled():
"""Test create_span yields None when OTEL is disabled."""
with patch("stampbot.telemetry.settings") as mock_settings:
mock_settings.otel_enabled = False
from stampbot.telemetry import create_span
with create_span("test_span", {"attr": "value"}) as span:
> assert span is None
E assert NonRecordingSpan(SpanContext(trace_id=0x00000000000000000000000000000000, span_id=0x0000000000000000, trace_flags=0x00, trace_state=[], is_remote=False)) is None
tests/test_telemetry.py:99: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_create_span_when_otel_disabled - assert NonRecordingSpan(SpanContext(trace_id=0x00000000000000000000000000000000, span_id=0x0000000000000000, trace_flags=0x00, trace_state=[], is_remote=False)) is None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 323 passed, 3 deselected, 1 warning in 3.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 7, definition_name: create_span
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -172,7 +172,7 @@
with tracer.start_as_current_span(name) as span:
try:
- if attributes:
+ if not attributes:
for key, value in attributes.items():
span.set_attribute(key, value)
yield span........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................F
=================================== FAILURES ===================================
_______________________ test_create_span_with_exception ________________________
def test_create_span_with_exception():
"""Test create_span records exception when one is raised."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.get_tracer") as mock_get_tracer,
):
mock_settings.otel_enabled = True
mock_span = Mock()
mock_tracer = Mock()
mock_tracer.start_as_current_span.return_value.__enter__ = Mock(return_value=mock_span)
mock_tracer.start_as_current_span.return_value.__exit__ = Mock(return_value=False)
mock_get_tracer.return_value = mock_tracer
from stampbot.telemetry import create_span
test_error = ValueError("test error")
try:
> with create_span("test_span"):
^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_telemetry.py:366:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:141: in __enter__
return next(self.gen)
^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
name = 'test_span', attributes = None, record_exception = True
@contextmanager
def create_span(
name: str,
attributes: dict[str, Any] | None = None,
record_exception: bool = True,
) -> Iterator[Span | None]:
"""Create a span context manager for tracing operations.
This is a convenience wrapper that handles span creation, attribute setting,
and exception recording. If OpenTelemetry is disabled, yields None and
the code block executes without tracing.
Args:
name: Name of the span
attributes: Optional attributes to set on the span
record_exception: Whether to record exceptions on the span
Yields:
The span if telemetry is enabled, None otherwise
Example:
with create_span("github.approve_pr", {"repo": "owner/repo", "pr": 123}) as span:
# do work
if span:
span.set_attribute("result", "success")
"""
if not settings.otel_enabled:
yield None
return
tracer = get_tracer("stampbot")
with tracer.start_as_current_span(name) as span:
try:
if not attributes:
> for key, value in attributes.items():
^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'items'
stampbot/telemetry.py:176: AttributeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_create_span_with_exception - AttributeError: 'NoneType' object has no attribute 'items'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 336 passed, 3 deselected, 1 warning in 3.95s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 8, definition_name: create_span
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -177,7 +177,7 @@
span.set_attribute(key, value)
yield span
except Exception as e:
- if record_exception:
+ if not record_exception:
span.record_exception(e)
span.set_status(Status(StatusCode.ERROR, str(e)))
raise........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................F
=================================== FAILURES ===================================
_______________________ test_create_span_with_exception ________________________
def test_create_span_with_exception():
"""Test create_span records exception when one is raised."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.get_tracer") as mock_get_tracer,
):
mock_settings.otel_enabled = True
mock_span = Mock()
mock_tracer = Mock()
mock_tracer.start_as_current_span.return_value.__enter__ = Mock(return_value=mock_span)
mock_tracer.start_as_current_span.return_value.__exit__ = Mock(return_value=False)
mock_get_tracer.return_value = mock_tracer
from stampbot.telemetry import create_span
test_error = ValueError("test error")
try:
with create_span("test_span"):
raise test_error
except ValueError:
pass
> mock_span.record_exception.assert_called_once_with(test_error)
tests/test_telemetry.py:371:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='mock.record_exception' id='140294943374176'>
args = (ValueError('test error'),), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'record_exception' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_create_span_with_exception - AssertionError: Expected 'record_exception' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 336 passed, 3 deselected, 1 warning in 4.00s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 9, definition_name: set_span_error
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -190,7 +190,7 @@
span: The span to update (can be None if telemetry disabled)
error: The exception that occurred
"""
- if span:
+ if not span:
span.record_exception(error)
span.set_status(Status(StatusCode.ERROR, str(error)))
........................................................................ [ 15%]
..................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_failure _____________________
self = <stampbot.github_client.GitHubAppClient object at 0x7fc6a6a0d3b0>
installation_id = 123456, repo_full_name = 'owner/repo', pr_number = 42
comment = 'Auto-approved by Stampbot'
def approve_pr(
self,
installation_id: int,
repo_full_name: str,
pr_number: int,
comment: str = "Auto-approved by Stampbot",
) -> bool:
"""Approve a pull request.
Args:
installation_id: GitHub App installation ID
repo_full_name: Repository full name (owner/repo)
pr_number: Pull request number
comment: Review comment
Returns:
True if successful, False otherwise
"""
start_time = time.time()
with create_span(
"github.approve_pr",
{
"github.repo": repo_full_name,
"github.pr_number": pr_number,
"github.installation_id": installation_id,
},
) as span:
try:
client = self._get_installation_client(installation_id)
repo = client.get_repo(repo_full_name)
pr = repo.get_pull(pr_number)
# Create approval review
> pr.create_review(
body=comment,
event="APPROVE",
)
stampbot/github_client.py:437:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='Github().get_repo().get_pull().create_review' id='140491176395504'>
args = (), kwargs = {'body': 'Auto-approved by Stampbot', 'event': 'APPROVE'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: API Error
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
self = <tests.test_github_client.TestApprovePR object at 0x7fc6a9bb42d0>
def test_approve_pr_failure(self):
"""Test PR approval failure."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_pr.create_review.side_effect = Exception("API Error")
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
> result = client.approve_pr(123456, "owner/repo", 42)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_github_client.py:668:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/github_client.py:466: in approve_pr
set_span_error(span, e)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
span = None, error = Exception('API Error')
def set_span_error(span: Span | None, error: Exception) -> None:
"""Set error status on a span.
Args:
span: The span to update (can be None if telemetry disabled)
error: The exception that occurred
"""
if not span:
> span.record_exception(error)
^^^^^^^^^^^^^^^^^^^^^
E AttributeError: 'NoneType' object has no attribute 'record_exception'
stampbot/telemetry.py:194: AttributeError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_failure - AttributeError: 'NoneType' object has no attribute 'record_exception'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 122 passed, 3 deselected, 1 warning in 3.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 10, definition_name: set_span_ok
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -201,7 +201,7 @@
Args:
span: The span to update (can be None if telemetry disabled)
"""
- if span:
+ if not span:
span.set_status(Status(StatusCode.OK))
........................................................................ [ 15%]
.................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_success _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7fdc6a340190>
def test_approve_pr_success(self):
"""Test successful PR approval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is True
E assert False is True
tests/test_github_client.py:631: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:22:43.919736Z[0m [[31m[1merror [0m] [1mFailed to approve PR #42 in owner/repo: 'NoneType' object has no attribute 'set_status'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object has no attribute 'set_status'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:468 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object has no attribute 'set_status'"}, 'event': "Failed to approve PR #42 in owner/repo: 'NoneType' object has no attribute 'set_status'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:22:43.919736Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 121 passed, 3 deselected, 1 warning in 2.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 11, definition_name: add_span_attributes
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -212,7 +212,7 @@
span: The span to update (can be None if telemetry disabled)
attributes: Attributes to add
"""
- if span:
+ if not span:
for key, value in attributes.items():
span.set_attribute(key, value)
........................................................................ [ 15%]
.................................................F
=================================== FAILURES ===================================
____________________ TestApprovePR.test_approve_pr_success _____________________
self = <tests.test_github_client.TestApprovePR object at 0x7f3cb56a4190>
def test_approve_pr_success(self):
"""Test successful PR approval."""
with (
patch("stampbot.github_client.is_configured", return_value=True),
patch("stampbot.github_client.settings") as mock_settings,
patch("stampbot.github_client.Auth.AppAuth"),
patch("stampbot.github_client.GithubIntegration") as mock_integration_cls,
patch("stampbot.github_client.Github") as mock_github_cls,
patch("stampbot.github_client.create_span") as mock_span,
):
mock_settings.app_id = 12345
mock_settings.private_key = TEST_PEM_KEY
mock_settings.otel_enabled = False
mock_integration = Mock()
mock_token = Mock()
mock_token.token = TEST_TOKEN
mock_integration.get_access_token.return_value = mock_token
mock_integration_cls.return_value = mock_integration
mock_pr = Mock()
mock_repo = Mock()
mock_repo.get_pull.return_value = mock_pr
mock_github = Mock()
mock_github.get_repo.return_value = mock_repo
mock_github.get_rate_limit.return_value = Mock(core=Mock(remaining=4500, limit=5000))
mock_github_cls.return_value = mock_github
mock_span.return_value.__enter__ = Mock(return_value=None)
mock_span.return_value.__exit__ = Mock(return_value=False)
from stampbot.github_client import GitHubAppClient
client = GitHubAppClient()
result = client.approve_pr(123456, "owner/repo", 42)
> assert result is True
E assert False is True
tests/test_github_client.py:631: AssertionError
----------------------------- Captured stdout call -----------------------------
[2m2026-09-04T22:13:07.524250Z[0m [[31m[1merror [0m] [1mFailed to approve PR #42 in owner/repo: 'NoneType' object has no attribute 'set_attribute'[0m [[0m[1m[34mstampbot.github_client[0m][0m [36m_logger[0m=[35m<_FixedFindCallerLogger stampbot.github_client (INFO)>[0m [36m_name[0m=[35merror[0m [36mextra[0m=[35m{'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object has no attribute 'set_attribute'"}[0m
------------------------------ Captured log call -------------------------------
ERROR stampbot.github_client:github_client.py:468 {'extra': {'repo': 'owner/repo', 'pr_number': 42, 'installation_id': 123456, 'error': "'NoneType' object has no attribute 'set_attribute'"}, 'event': "Failed to approve PR #42 in owner/repo: 'NoneType' object has no attribute 'set_attribute'", 'logger': 'stampbot.github_client', 'level': 'error', 'timestamp': '2026-09-04T22:13:07.524250Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_github_client.py::TestApprovePR::test_approve_pr_success - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 121 passed, 3 deselected, 1 warning in 2.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 0, definition_name: configure_telemetry
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -45,7 +45,7 @@
# Log records gain trace and span IDs whenever tracing is enabled. This
# runs after the provider attempt so the records also carry the service
# name when an exporter exists.
- LoggingInstrumentor().instrument(set_logging_format=True)
+ LoggingInstrumentor().instrument(set_logging_format=False)
return provider
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.....................................F
=================================== FAILURES ===================================
_____________________ test_configure_telemetry_no_endpoint _____________________
library_instrumentors = (<MagicMock name='RequestsInstrumentor' id='140028010868624'>, <MagicMock name='LoggingInstrumentor' id='140028009664528'>)
def test_configure_telemetry_no_endpoint(library_instrumentors):
"""Test a missing endpoint exports nothing but still shapes log records."""
requests_instrumentor, logging_instrumentor = library_instrumentors
with patch("stampbot.telemetry.settings") as mock_settings:
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = None
from stampbot.telemetry import configure_telemetry
result = configure_telemetry()
assert result is None
> logging_instrumentor.return_value.instrument.assert_called_once_with(set_logging_format=True)
tests/test_telemetry.py:124:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='LoggingInstrumentor().instrument' id='140028009667552'>
args = (), kwargs = {'set_logging_format': True}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: instrument(set_logging_format=True)
E Actual: instrument(set_logging_format=False)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"event": "OpenTelemetry enabled but no endpoint configured", "logger": "stampbot.telemetry", "level": "warning", "timestamp": "2026-09-04T20:18:33.249815Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "warning"}
------------------------------ Captured log call -------------------------------
WARNING stampbot.telemetry:telemetry.py:60 {'event': 'OpenTelemetry enabled but no endpoint configured', 'logger': 'stampbot.telemetry', 'level': 'warning', 'timestamp': '2026-09-04T20:18:33.249815Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_no_endpoint - AssertionError: expected call not found.
Expected: instrument(set_logging_format=True)
Actual: instrument(set_logging_format=False)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 325 passed, 3 deselected, 1 warning in 4.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 1, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -73,7 +73,7 @@
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
- otel_insecure_requested = settings.get("otel_insecure", False) is True
+ otel_insecure_requested = settings.get("otel_insecure", False) is False
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.......................................F
=================================== FAILURES ===================================
_______________________ test_configure_telemetry_success _______________________
def test_configure_telemetry_success():
"""Test configure_telemetry returns provider when properly configured."""
from stampbot.version import APP_VERSION
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource") as mock_resource,
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor") as mock_processor,
patch("stampbot.telemetry.trace") as mock_trace,
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://localhost:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = False
mock_provider = Mock()
mock_provider_cls.return_value = mock_provider
from stampbot.telemetry import configure_telemetry
result = configure_telemetry()
assert result == mock_provider
mock_resource.create.assert_called_once_with(
{"service.name": "test-service", "service.version": APP_VERSION}
)
mock_provider_cls.assert_called_once()
> mock_exporter.assert_called_once_with(
endpoint="http://localhost:4317",
insecure=False,
)
tests/test_telemetry.py:168:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140152203066208'>, args = ()
kwargs = {'endpoint': 'http://localhost:4317', 'insecure': False}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
E Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "http://localhost:4317", "otel_transport": "plaintext"}, "event": "OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T22:18:56.612750Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://localhost:4317', 'otel_transport': 'plaintext'}, 'event': 'OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T22:18:56.612750Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_success - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 327 passed, 3 deselected, 1 warning in 4.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 2, definition_name: create_span
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -142,7 +142,7 @@
def create_span(
name: str,
attributes: dict[str, Any] | None = None,
- record_exception: bool = True,
+ record_exception: bool = False,
) -> Iterator[Span | None]:
"""Create a span context manager for tracing operations.
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................F
=================================== FAILURES ===================================
_______________________ test_create_span_with_exception ________________________
def test_create_span_with_exception():
"""Test create_span records exception when one is raised."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.get_tracer") as mock_get_tracer,
):
mock_settings.otel_enabled = True
mock_span = Mock()
mock_tracer = Mock()
mock_tracer.start_as_current_span.return_value.__enter__ = Mock(return_value=mock_span)
mock_tracer.start_as_current_span.return_value.__exit__ = Mock(return_value=False)
mock_get_tracer.return_value = mock_tracer
from stampbot.telemetry import create_span
test_error = ValueError("test error")
try:
with create_span("test_span"):
raise test_error
except ValueError:
pass
> mock_span.record_exception.assert_called_once_with(test_error)
tests/test_telemetry.py:371:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Mock name='mock.record_exception' id='139831661656928'>
args = (ValueError('test error'),), kwargs = {}
def assert_called_once_with(self, /, *args, **kwargs):
"""assert that the mock was called exactly once and that that call was
with the specified arguments."""
if not self.call_count == 1:
msg = ("Expected '%s' to be called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
> raise AssertionError(msg)
E AssertionError: Expected 'record_exception' to be called once. Called 0 times.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:997: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_create_span_with_exception - AssertionError: Expected 'record_exception' to be called once. Called 0 times.
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 336 passed, 3 deselected, 1 warning in 4.02s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceFalseWithTrue, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -73,7 +73,7 @@
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
- otel_insecure_requested = settings.get("otel_insecure", False) is True
+ otel_insecure_requested = settings.get("otel_insecure", True) is True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceAndWithOr, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -75,7 +75,7 @@
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
- if otel_insecure_requested and endpoint_uses_https:
+ if otel_insecure_requested or endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https
otlp_exporter = OTLPSpanExporter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceAndWithOr, occurrence: 1, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -77,7 +77,7 @@
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
- otel_insecure = otel_insecure_requested and not endpoint_uses_https
+ otel_insecure = otel_insecure_requested or not endpoint_uses_https
otlp_exporter = OTLPSpanExporter(
endpoint=settings.otel_endpoint,
insecure=otel_insecure,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.......................................F
=================================== FAILURES ===================================
_______________________ test_configure_telemetry_success _______________________
def test_configure_telemetry_success():
"""Test configure_telemetry returns provider when properly configured."""
from stampbot.version import APP_VERSION
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource") as mock_resource,
patch("stampbot.telemetry.TracerProvider") as mock_provider_cls,
patch("stampbot.telemetry.OTLPSpanExporter") as mock_exporter,
patch("stampbot.telemetry.BatchSpanProcessor") as mock_processor,
patch("stampbot.telemetry.trace") as mock_trace,
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://localhost:4317"
mock_settings.otel_service_name = "test-service"
mock_settings.get.return_value = False
mock_provider = Mock()
mock_provider_cls.return_value = mock_provider
from stampbot.telemetry import configure_telemetry
result = configure_telemetry()
assert result == mock_provider
mock_resource.create.assert_called_once_with(
{"service.name": "test-service", "service.version": APP_VERSION}
)
mock_provider_cls.assert_called_once()
> mock_exporter.assert_called_once_with(
endpoint="http://localhost:4317",
insecure=False,
)
tests/test_telemetry.py:168:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:998: in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='OTLPSpanExporter' id='140445169957728'>, args = ()
kwargs = {'endpoint': 'http://localhost:4317', 'insecure': False}
def assert_called_with(self, /, *args, **kwargs):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
if self.call_args is None:
expected = self._format_mock_call_signature(args, kwargs)
actual = 'not called.'
error_message = ('expected call not found.\nExpected: %s\n Actual: %s'
% (expected, actual))
raise AssertionError(error_message)
def _error_message():
msg = self._format_mock_failure_message(args, kwargs)
return msg
expected = self._call_matcher(_Call((args, kwargs), two=True))
actual = self._call_matcher(self.call_args)
if actual != expected:
cause = expected if isinstance(expected, Exception) else None
> raise AssertionError(_error_message()) from cause
E AssertionError: expected call not found.
E Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
E Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:986: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"otel_endpoint": "http://localhost:4317", "otel_transport": "plaintext"}, "event": "OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)", "logger": "stampbot.telemetry", "level": "info", "timestamp": "2026-09-04T23:03:47.841843Z", "_logger": "<_FixedFindCallerLogger stampbot.telemetry (INFO)>", "_name": "info"}
------------------------------ Captured log call -------------------------------
INFO stampbot.telemetry:telemetry.py:98 {'extra': {'otel_endpoint': 'http://localhost:4317', 'otel_transport': 'plaintext'}, 'event': 'OpenTelemetry configured with endpoint: http://localhost:4317 (plaintext)', 'logger': 'stampbot.telemetry', 'level': 'info', 'timestamp': '2026-09-04T23:03:47.841843Z'}
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_success - AssertionError: expected call not found.
Expected: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=False)
Actual: OTLPSpanExporter(endpoint='http://localhost:4317', insecure=True)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 327 passed, 3 deselected, 1 warning in 4.13s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 0, definition_name: _configure_exporter
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -107,7 +107,7 @@
return provider
- except Exception as e:
+ except CosmicRayTestingException as e:
logger.error("Failed to configure OpenTelemetry: %s", e, extra={"error": str(e)})
return None
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.............................................F
=================================== FAILURES ===================================
______________________ test_configure_telemetry_exception ______________________
def _configure_exporter() -> TracerProvider | None:
"""Create the tracer provider and OTLP exporter.
Returns:
TracerProvider when an endpoint is configured and setup succeeds, else None.
"""
if not settings.otel_endpoint:
logger.warning("OpenTelemetry enabled but no endpoint configured")
return None
try:
# Create resource
> resource = Resource.create(
{
"service.name": settings.otel_service_name,
"service.version": APP_VERSION,
}
)
stampbot/telemetry.py:65:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='Resource.create' id='140607186104544'>
args = ({'service.name': <MagicMock name='settings.otel_service_name' id='140607186093792'>, 'service.version': '0.1.0'},)
kwargs = {}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Connection failed
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
def test_configure_telemetry_exception():
"""Test configure_telemetry handles exceptions gracefully."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.Resource") as mock_resource,
):
mock_settings.otel_enabled = True
mock_settings.otel_endpoint = "http://localhost:4317"
mock_resource.create.side_effect = Exception("Connection failed")
from stampbot.telemetry import configure_telemetry
> result = configure_telemetry()
^^^^^^^^^^^^^^^^^^^^^
tests/test_telemetry.py:309:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/telemetry.py:43: in configure_telemetry
provider = _configure_exporter()
^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def _configure_exporter() -> TracerProvider | None:
"""Create the tracer provider and OTLP exporter.
Returns:
TracerProvider when an endpoint is configured and setup succeeds, else None.
"""
if not settings.otel_endpoint:
logger.warning("OpenTelemetry enabled but no endpoint configured")
return None
try:
# Create resource
resource = Resource.create(
{
"service.name": settings.otel_service_name,
"service.version": APP_VERSION,
}
)
# Create tracer provider
provider = TracerProvider(resource=resource)
# TLS is the default. Plaintext export requires an explicit opt-in.
otel_insecure_requested = settings.get("otel_insecure", False) is True
endpoint_uses_https = urlparse(settings.otel_endpoint).scheme == "https"
if otel_insecure_requested and endpoint_uses_https:
logger.warning("Ignoring plaintext OTLP setting for an HTTPS endpoint")
otel_insecure = otel_insecure_requested and not endpoint_uses_https
otlp_exporter = OTLPSpanExporter(
endpoint=settings.otel_endpoint,
insecure=otel_insecure,
)
# Add span processor
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
# Set as global tracer provider
trace.set_tracer_provider(provider)
# PyGithub uses requests, so this gives every GitHub API call its own
# child span with method, URL, and status code. Without it a
# ``github.*`` span hides how many requests it made and which one was
# slow.
RequestsInstrumentor().instrument()
logger.info(
"OpenTelemetry configured with endpoint: %s (%s)",
settings.otel_endpoint,
"plaintext" if otel_insecure else "TLS",
extra={
"otel_endpoint": settings.otel_endpoint,
"otel_transport": "plaintext" if otel_insecure else "TLS",
},
)
return provider
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/telemetry.py:110: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_configure_telemetry_exception - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 333 passed, 3 deselected, 1 warning in 4.10s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 1, definition_name: instrument_fastapi
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -122,7 +122,7 @@
try:
FastAPIInstrumentor.instrument_app(app, excluded_urls=TRACE_EXCLUDED_URLS)
logger.info("FastAPI instrumented with OpenTelemetry")
- except Exception as e:
+ except CosmicRayTestingException as e:
logger.error("Failed to instrument FastAPI: %s", e, extra={"error": str(e)})
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
...............................................F
=================================== FAILURES ===================================
______________________ test_instrument_fastapi_exception _______________________
app = <Mock id='139871416464592'>
def instrument_fastapi(app: Any) -> None:
"""Instrument FastAPI app with OpenTelemetry.
Args:
app: FastAPI application instance
"""
if settings.otel_enabled:
try:
> FastAPIInstrumentor.instrument_app(app, excluded_urls=TRACE_EXCLUDED_URLS)
stampbot/telemetry.py:123:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1176: in __call__
return self._mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1180: in _mock_call
return self._execute_mock_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <MagicMock name='FastAPIInstrumentor.instrument_app' id='139871416462576'>
args = (<Mock id='139871416464592'>,)
kwargs = {'excluded_urls': '/health$,/ready$'}
def _execute_mock_call(self, /, *args, **kwargs):
# separate from _increment_mock_call so that awaited functions are
# executed separately from their call, also AsyncMock overrides this method
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E Exception: Instrumentation failed
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py:1241: Exception
During handling of the above exception, another exception occurred:
def test_instrument_fastapi_exception():
"""Test instrument_fastapi handles exceptions gracefully."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.FastAPIInstrumentor") as mock_instrumentor,
):
mock_settings.otel_enabled = True
mock_instrumentor.instrument_app.side_effect = Exception("Instrumentation failed")
from stampbot.telemetry import instrument_fastapi
mock_app = Mock()
# Should not raise
> instrument_fastapi(mock_app)
tests/test_telemetry.py:345:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app = <Mock id='139871416464592'>
def instrument_fastapi(app: Any) -> None:
"""Instrument FastAPI app with OpenTelemetry.
Args:
app: FastAPI application instance
"""
if settings.otel_enabled:
try:
FastAPIInstrumentor.instrument_app(app, excluded_urls=TRACE_EXCLUDED_URLS)
logger.info("FastAPI instrumented with OpenTelemetry")
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/telemetry.py:125: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_instrument_fastapi_exception - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 335 passed, 3 deselected, 1 warning in 4.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 2, definition_name: create_span
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -176,7 +176,7 @@
for key, value in attributes.items():
span.set_attribute(key, value)
yield span
- except Exception as e:
+ except CosmicRayTestingException as e:
if record_exception:
span.record_exception(e)
span.set_status(Status(StatusCode.ERROR, str(e)))........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
................................................F
=================================== FAILURES ===================================
_______________________ test_create_span_with_exception ________________________
def test_create_span_with_exception():
"""Test create_span records exception when one is raised."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.get_tracer") as mock_get_tracer,
):
mock_settings.otel_enabled = True
mock_span = Mock()
mock_tracer = Mock()
mock_tracer.start_as_current_span.return_value.__enter__ = Mock(return_value=mock_span)
mock_tracer.start_as_current_span.return_value.__exit__ = Mock(return_value=False)
mock_get_tracer.return_value = mock_tracer
from stampbot.telemetry import create_span
test_error = ValueError("test error")
try:
with create_span("test_span"):
> raise test_error
E ValueError: test error
tests/test_telemetry.py:367: ValueError
During handling of the above exception, another exception occurred:
def test_create_span_with_exception():
"""Test create_span records exception when one is raised."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.get_tracer") as mock_get_tracer,
):
mock_settings.otel_enabled = True
mock_span = Mock()
mock_tracer = Mock()
mock_tracer.start_as_current_span.return_value.__enter__ = Mock(return_value=mock_span)
mock_tracer.start_as_current_span.return_value.__exit__ = Mock(return_value=False)
mock_get_tracer.return_value = mock_tracer
from stampbot.telemetry import create_span
test_error = ValueError("test error")
try:
> with create_span("test_span"):
^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_telemetry.py:366:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/contextlib.py:162: in __exit__
self.gen.throw(value)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
name = 'test_span', attributes = None, record_exception = True
@contextmanager
def create_span(
name: str,
attributes: dict[str, Any] | None = None,
record_exception: bool = True,
) -> Iterator[Span | None]:
"""Create a span context manager for tracing operations.
This is a convenience wrapper that handles span creation, attribute setting,
and exception recording. If OpenTelemetry is disabled, yields None and
the code block executes without tracing.
Args:
name: Name of the span
attributes: Optional attributes to set on the span
record_exception: Whether to record exceptions on the span
Yields:
The span if telemetry is enabled, None otherwise
Example:
with create_span("github.approve_pr", {"repo": "owner/repo", "pr": 123}) as span:
# do work
if span:
span.set_attribute("result", "success")
"""
if not settings.otel_enabled:
yield None
return
tracer = get_tracer("stampbot")
with tracer.start_as_current_span(name) as span:
try:
if attributes:
for key, value in attributes.items():
span.set_attribute(key, value)
yield span
> except CosmicRayTestingException as e:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/telemetry.py:179: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_create_span_with_exception - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 336 passed, 3 deselected, 1 warning in 4.04s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/RemoveDecorator, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -136,9 +136,6 @@
Tracer instance (no-op if telemetry disabled)
"""
return trace.get_tracer(name)
-
-
-@contextmanager
def create_span(
name: str,
attributes: dict[str, Any] | None = None,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..F
=================================== FAILURES ===================================
___________________________ test_webhook_valid_ping ____________________________
test_client = <starlette.testclient.TestClient object at 0x7f67249b59d0>
def test_webhook_valid_ping(test_client: TestClient):
"""Test webhook accepts valid ping event."""
import hashlib
import hmac
from stampbot.webhook_handler import webhook_handler
body = json.dumps({"zen": "Design for failure."}).encode()
signature = (
"sha256="
+ hmac.new(
webhook_handler.webhook_secret,
body,
hashlib.sha256,
).hexdigest()
)
response = test_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "ping",
"X-Hub-Signature-256": signature,
},
)
> assert response.status_code == 200
E assert 500 == 200
E + where 500 = <Response [500 Internal Server Error]>.status_code
tests/test_main.py:360: AssertionError
----------------------------- Captured stdout call -----------------------------
{"extra": {"error": "'generator' object does not support the context manager protocol (missed __exit__ method)"}, "event": "Error handling webhook event: 'generator' object does not support the context manager protocol (missed __exit__ method)", "client_ip": "testclient", "logger": "stampbot.main", "level": "error", "timestamp": "2026-09-04T22:07:31.322751Z", "_logger": "<_FixedFindCallerLogger stampbot.main (INFO)>", "_name": "error"}
{"event": "HTTP Request: POST http://testserver/webhook \"HTTP/1.1 500 Internal Server Error\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T22:07:31.324115Z"}
------------------------------ Captured log call -------------------------------
ERROR stampbot.main:main.py:513 {'extra': {'error': "'generator' object does not support the context manager protocol (missed __exit__ method)"}, 'event': "Error handling webhook event: 'generator' object does not support the context manager protocol (missed __exit__ method)", 'client_ip': 'testclient', 'logger': 'stampbot.main', 'level': 'error', 'timestamp': '2026-09-04T22:07:31.322751Z'}
INFO httpx:_client.py:1025 HTTP Request: POST http://testserver/webhook "HTTP/1.1 500 Internal Server Error"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_main.py::test_webhook_valid_ping - assert 500 == 200
+ where 500 = <Response [500 Internal Server Error]>.status_code
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 218 passed, 3 deselected, 1 warning in 3.55s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 0, definition_name: create_span
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -173,7 +173,7 @@
with tracer.start_as_current_span(name) as span:
try:
if attributes:
- for key, value in attributes.items():
+ for key, value in []:
span.set_attribute(key, value)
yield span
except Exception as e:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
.................................................F
=================================== FAILURES ===================================
________________ test_create_span_with_attributes_when_enabled _________________
def test_create_span_with_attributes_when_enabled():
"""Test create_span sets attributes on span when OTEL is enabled."""
with (
patch("stampbot.telemetry.settings") as mock_settings,
patch("stampbot.telemetry.get_tracer") as mock_get_tracer,
):
mock_settings.otel_enabled = True
mock_span = Mock()
mock_tracer = Mock()
mock_tracer.start_as_current_span.return_value.__enter__ = Mock(return_value=mock_span)
mock_tracer.start_as_current_span.return_value.__exit__ = Mock(return_value=False)
mock_get_tracer.return_value = mock_tracer
from stampbot.telemetry import create_span
with create_span("test_span", {"key1": "value1", "key2": "value2"}) as span:
assert span == mock_span
# Verify attributes were set on the span
> assert mock_span.set_attribute.call_count == 2
E AssertionError: assert 0 == 2
E + where 0 = <Mock name='mock.set_attribute' id='140520429603536'>.call_count
E + where <Mock name='mock.set_attribute' id='140520429603536'> = <Mock id='140520429610256'>.set_attribute
tests/test_telemetry.py:395: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_create_span_with_attributes_when_enabled - AssertionError: assert 0 == 2
+ where 0 = <Mock name='mock.set_attribute' id='140520429603536'>.call_count
+ where <Mock name='mock.set_attribute' id='140520429603536'> = <Mock id='140520429610256'>.set_attribute
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 337 passed, 3 deselected, 1 warning in 3.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ZeroIterationForLoop, occurrence: 1, definition_name: add_span_attributes
--- mutation diff ---
--- a/stampbot/telemetry.py
+++ b/stampbot/telemetry.py
@@ -213,6 +213,6 @@
attributes: Attributes to add
"""
if span:
- for key, value in attributes.items():
+ for key, value in []:
span.set_attribute(key, value)
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
..................................F
=================================== FAILURES ===================================
______________________ test_add_span_attributes_with_span ______________________
def test_add_span_attributes_with_span():
"""Test add_span_attributes adds attributes to span."""
from stampbot.telemetry import add_span_attributes
mock_span = Mock()
attributes = {"key1": "value1", "key2": "value2"}
add_span_attributes(mock_span, attributes)
> assert mock_span.set_attribute.call_count == 2
E AssertionError: assert 0 == 2
E + where 0 = <Mock name='mock.set_attribute' id='140336215374352'>.call_count
E + where <Mock name='mock.set_attribute' id='140336215374352'> = <Mock id='140336215361248'>.set_attribute
tests/test_telemetry.py:87: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_telemetry.py::test_add_span_attributes_with_span - AssertionError: assert 0 == 2
+ where 0 = <Mock name='mock.set_attribute' id='140336215374352'>.call_count
+ where <Mock name='mock.set_attribute' id='140336215374352'> = <Mock id='140336215361248'>.set_attribute
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 322 passed, 3 deselected, 1 warning in 3.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 0, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=( 1.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.72s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 1, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=( -0.995, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [-0.995, 0.01....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
E
E At index 0 diff: -0.995 != 0.005
E
E Full diff:
E [
E - 0.005,
E ? ^^
E + -0.995,
E ? + ^^
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [-0.995, 0.01....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
At index 0 diff: -0.995 != 0.005
Full diff:
[
- 0.005,
? ^^
+ -0.995,
? + ^^
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 2, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 1.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 3, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, -0.99, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 4, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 1.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 5, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, -0.975, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 6, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 1.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 7, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, -0.95, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.73s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 8, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 1.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 9, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, -0.9, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 10, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 1.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 11, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, -0.75, 0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 12, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 1.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 13, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, -0.5, 1.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 14, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 2.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
E
E At index 7 diff: 2.0 != 1.0
E
E Full diff:
E [
E 0.005,
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E - 1.0,
E ? ^
E + 2.0,
E ? ^
E 2.5,
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
At index 7 diff: 2.0 != 1.0
Full diff:
[
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
- 1.0,
? ^
+ 2.0,
? ^
2.5,
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 15, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 0.0, 2.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:40: in <module>
http_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 16, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 3.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
E
E At index 8 diff: 3.5 != 2.5
E
E Full diff:
E [
E 0.005,
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 3.5,
E ? ^
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
At index 8 diff: 3.5 != 2.5
Full diff:
[
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 3.5,
? ^
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 17, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 1.5, 5.0, 10.0),
)
http_request_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
E
E At index 8 diff: 1.5 != 2.5
E
E Full diff:
E [
E 0.005,
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 1.5,
E ? ^
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
At index 8 diff: 1.5 != 2.5
Full diff:
[
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 1.5,
? ^
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 18, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 6.0, 10.0),
)
http_request_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
E
E At index 9 diff: 6.0 != 5.0
E
E Full diff:
E [
E 0.005,
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 6.0,
E ? ^
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
At index 9 diff: 6.0 != 5.0
Full diff:
[
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 6.0,
? ^
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 19, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 4.0, 10.0),
)
http_request_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
E
E At index 9 diff: 4.0 != 5.0
E
E Full diff:
E [
E 0.005,
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 4.0,
E ? ^
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
At index 9 diff: 4.0 != 5.0
Full diff:
[
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 4.0,
? ^
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 20, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 11.0),
)
http_request_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
E
E At index 10 diff: 11.0 != 10.0
E
E Full diff:
E [
E 0.005,
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^
E + 11.0,
E ? ^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
At index 10 diff: 11.0 != 10.0
Full diff:
[
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^
+ 11.0,
? ^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 21, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -41,7 +41,7 @@
"stampbot_http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint"],
- buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 9.0),
)
http_request_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
E
E At index 10 diff: 9.0 != 10.0
E
E Full diff:
E [
E 0.005,
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^^
E + 9.0,
E ? ^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.005, 0.01,....1, 0.25, ...] == [0.005, 0.01,....1, 0.25, ...]
At index 10 diff: 9.0 != 10.0
Full diff:
[
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^^
+ 9.0,
? ^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 22, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=( 101, 500, 1000, 5000, 10000, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [101.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 0 diff: 101.0 != 100
E
E Full diff:
E [
E - 100,
E + 101.0,
E ? ++
E - 500,
E - 1000,
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [101.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 0 diff: 101.0 != 100
Full diff:
[
- 100,
+ 101.0,
? ++
- 500,
- 1000,
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 23, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=( 99, 500, 1000, 5000, 10000, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [99.0, 500.0,... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 0 diff: 99.0 != 100
E
E Full diff:
E [
E + 99.0,
E - 100,
E - 500,
E - 1000,
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [99.0, 500.0,... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 0 diff: 99.0 != 100
Full diff:
[
+ 99.0,
- 100,
- 500,
- 1000,
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 24, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 501, 1000, 5000, 10000, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 501.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 1 diff: 501.0 != 500
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E ? ^
E + 501.0,
E ? ^^
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 501.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 1 diff: 501.0 != 500
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
? ^
+ 501.0,
? ^^
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 25, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 499, 1000, 5000, 10000, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 499.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 1 diff: 499.0 != 500
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 499.0,
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 499.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 1 diff: 499.0 != 500
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 499.0,
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 26, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1001, 5000, 10000, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 2 diff: 1001.0 != 1000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E ? ^
E + 1001.0,
E ? ^^
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 2 diff: 1001.0 != 1000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
? ^
+ 1001.0,
? ^^
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 27, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 999, 5000, 10000, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 2 diff: 999.0 != 1000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 999.0,
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 2 diff: 999.0 != 1000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 999.0,
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 28, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5001, 10000, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 3 diff: 5001.0 != 5000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E ? ^
E + 5001.0,
E ? ^^
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 3 diff: 5001.0 != 5000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
? ^
+ 5001.0,
? ^^
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 29, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 4999, 10000, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 3 diff: 4999.0 != 5000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 4999.0,
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 3 diff: 4999.0 != 5000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 4999.0,
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 30, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10001, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 4 diff: 10001.0 != 10000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E ? ^
E + 10001.0,
E ? ^^
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 4 diff: 10001.0 != 10000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
? ^
+ 10001.0,
? ^^
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 31, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 9999, 50000, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 4 diff: 9999.0 != 10000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 9999.0,
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 4 diff: 9999.0 != 10000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 9999.0,
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 32, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50001, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50001.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 5 diff: 50001.0 != 50000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E ? ^
E + 50001.0,
E ? ^^
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50001.0, ...] == [100, 500, 10...0, 50000, ...]
At index 5 diff: 50001.0 != 50000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
? ^
+ 50001.0,
? ^^
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.97s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 33, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 49999, 100000, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 49999.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 5 diff: 49999.0 != 50000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E + 49999.0,
E - 500000,
E ? ^
E + 100000.0,
E ? ^ ++
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 49999.0, ...] == [100, 500, 10...0, 50000, ...]
At index 5 diff: 49999.0 != 50000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
+ 49999.0,
- 500000,
? ^
+ 100000.0,
? ^ ++
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 34, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50000, 100001, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 6 diff: 100001.0 != 100000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100001.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 6 diff: 100001.0 != 100000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100001.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 35, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50000, 99999, 500000),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 6 diff: 99999.0 != 100000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 99999.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 6 diff: 99999.0 != 100000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 99999.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.87s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 36, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500001),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 7 diff: 500001.0 != 500000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500001.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 7 diff: 500001.0 != 500000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500001.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 37, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -48,7 +48,7 @@
"stampbot_http_request_size_bytes",
"HTTP request body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 499999),
)
http_response_size_bytes = Histogram(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 7 diff: 499999.0 != 500000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 499999.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 7 diff: 499999.0 != 500000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 499999.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 38, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=( 101, 500, 1000, 5000, 10000, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [101.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 0 diff: 101.0 != 100
E
E Full diff:
E [
E - 100,
E + 101.0,
E ? ++
E - 500,
E - 1000,
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [101.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 0 diff: 101.0 != 100
Full diff:
[
- 100,
+ 101.0,
? ++
- 500,
- 1000,
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 39, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=( 99, 500, 1000, 5000, 10000, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [99.0, 500.0,... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 0 diff: 99.0 != 100
E
E Full diff:
E [
E + 99.0,
E - 100,
E - 500,
E - 1000,
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [99.0, 500.0,... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 0 diff: 99.0 != 100
Full diff:
[
+ 99.0,
- 100,
- 500,
- 1000,
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 40, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 501, 1000, 5000, 10000, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 501.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 1 diff: 501.0 != 500
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E ? ^
E + 501.0,
E ? ^^
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 501.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 1 diff: 501.0 != 500
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
? ^
+ 501.0,
? ^^
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 41, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 499, 1000, 5000, 10000, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 499.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 1 diff: 499.0 != 500
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 499.0,
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 499.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 1 diff: 499.0 != 500
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 499.0,
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 42, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1001, 5000, 10000, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 2 diff: 1001.0 != 1000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E ? ^
E + 1001.0,
E ? ^^
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 2 diff: 1001.0 != 1000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
? ^
+ 1001.0,
? ^^
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 43, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 999, 5000, 10000, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 2 diff: 999.0 != 1000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 999.0,
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 2 diff: 999.0 != 1000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 999.0,
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 44, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5001, 10000, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 3 diff: 5001.0 != 5000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E ? ^
E + 5001.0,
E ? ^^
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 3 diff: 5001.0 != 5000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
? ^
+ 5001.0,
? ^^
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 45, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 4999, 10000, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 3 diff: 4999.0 != 5000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 4999.0,
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 3 diff: 4999.0 != 5000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 4999.0,
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 46, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10001, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 4 diff: 10001.0 != 10000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E ? ^
E + 10001.0,
E ? ^^
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 4 diff: 10001.0 != 10000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
? ^
+ 10001.0,
? ^^
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 47, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 9999, 50000, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 4 diff: 9999.0 != 10000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 9999.0,
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 4 diff: 9999.0 != 10000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 9999.0,
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 48, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50001, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50001.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 5 diff: 50001.0 != 50000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E ? ^
E + 50001.0,
E ? ^^
E + 100000.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50001.0, ...] == [100, 500, 10...0, 50000, ...]
At index 5 diff: 50001.0 != 50000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
? ^
+ 50001.0,
? ^^
+ 100000.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 49, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 49999, 100000, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 49999.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 5 diff: 49999.0 != 50000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E + 49999.0,
E - 500000,
E ? ^
E + 100000.0,
E ? ^ ++
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 49999.0, ...] == [100, 500, 10...0, 50000, ...]
At index 5 diff: 49999.0 != 50000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
+ 49999.0,
- 500000,
? ^
+ 100000.0,
? ^ ++
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 50, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50000, 100001, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 6 diff: 100001.0 != 100000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100001.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 6 diff: 100001.0 != 100000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100001.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.81s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 51, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50000, 99999, 500000),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 6 diff: 99999.0 != 100000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 99999.0,
E + 500000.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 6 diff: 99999.0 != 100000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 99999.0,
+ 500000.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 52, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500001),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 7 diff: 500001.0 != 500000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 500001.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 7 diff: 500001.0 != 500000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 500001.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.79s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 53, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -55,7 +55,7 @@
"stampbot_http_response_size_bytes",
"HTTP response body size in bytes",
["method", "endpoint"],
- buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
+ buckets=(100, 500, 1000, 5000, 10000, 50000, 100000, 499999),
)
http_requests_in_progress = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
E
E At index 7 diff: 499999.0 != 500000
E
E Full diff:
E [
E - 100,
E - 500,
E - 1000,
E + 100.0,
E ? +
E - 5000,
E + 500.0,
E ? +
E - 10000,
E + 1000.0,
E ? +
E - 50000,
E + 5000.0,
E ? +
E - 100000,
E + 10000.0,
E ? +
E - 500000,
E + 50000.0,
E ? +
E + 100000.0,
E + 499999.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [100.0, 500.0... 50000.0, ...] == [100, 500, 10...0, 50000, ...]
At index 7 diff: 499999.0 != 500000
Full diff:
[
- 100,
- 500,
- 1000,
+ 100.0,
? +
- 5000,
+ 500.0,
? +
- 10000,
+ 1000.0,
? +
- 50000,
+ 5000.0,
? +
- 100000,
+ 10000.0,
? +
- 500000,
+ 50000.0,
? +
+ 100000.0,
+ 499999.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 54, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=( 1.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 55, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=( -0.99, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [-0.99, 0.025....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
E
E At index 0 diff: -0.99 != 0.01
E
E Full diff:
E [
E - 0.01,
E + -0.99,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [-0.99, 0.025....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
At index 0 diff: -0.99 != 0.01
Full diff:
[
- 0.01,
+ -0.99,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.84s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 56, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 1.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 57, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, -0.975, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 58, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 1.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 59, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, -0.95, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 60, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 1.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 61, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, -0.9, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 62, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 1.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 63, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, -0.75, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 64, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 1.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 65, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, -0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 66, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 2.0, 2.5, 5.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
E
E At index 6 diff: 2.0 != 1.0
E
E Full diff:
E [
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E - 1.0,
E ? ^
E + 2.0,
E ? ^
E 2.5,
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
At index 6 diff: 2.0 != 1.0
Full diff:
[
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
- 1.0,
? ^
+ 2.0,
? ^
2.5,
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 67, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 0.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:83: in <module>
webhook_processing_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 68, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 3.5, 5.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
E
E At index 7 diff: 3.5 != 2.5
E
E Full diff:
E [
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 3.5,
E ? ^
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
At index 7 diff: 3.5 != 2.5
Full diff:
[
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 3.5,
? ^
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 69, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 1.5, 5.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
E
E At index 7 diff: 1.5 != 2.5
E
E Full diff:
E [
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 1.5,
E ? ^
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
At index 7 diff: 1.5 != 2.5
Full diff:
[
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 1.5,
? ^
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 70, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 6.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
E
E At index 8 diff: 6.0 != 5.0
E
E Full diff:
E [
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 6.0,
E ? ^
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
At index 8 diff: 6.0 != 5.0
Full diff:
[
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 6.0,
? ^
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 71, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 4.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
E
E At index 8 diff: 4.0 != 5.0
E
E Full diff:
E [
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 4.0,
E ? ^
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
At index 8 diff: 4.0 != 5.0
Full diff:
[
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 4.0,
? ^
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 72, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 11.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
E
E At index 9 diff: 11.0 != 10.0
E
E Full diff:
E [
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^
E + 11.0,
E ? ^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
At index 9 diff: 11.0 != 10.0
Full diff:
[
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^
+ 11.0,
? ^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.86s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 73, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -84,7 +84,7 @@
"stampbot_webhook_processing_duration_seconds",
"Webhook event processing duration in seconds",
["event_type"],
- buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 9.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
E
E At index 9 diff: 9.0 != 10.0
E
E Full diff:
E [
E 0.01,
E 0.025,
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^^
E + 9.0,
E ? ^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.01, 0.025,....25, 0.5, ...] == [0.01, 0.025,....25, 0.5, ...]
At index 9 diff: 9.0 != 10.0
Full diff:
[
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^^
+ 9.0,
? ^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 4.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 74, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=( 1.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:100: in <module>
pr_approval_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 75, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=( -0.9, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [-0.9, 0.25, ...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 0 diff: -0.9 != 0.1
E
E Full diff:
E [
E - 0.1,
E ? ^
E + -0.9,
E ? + ^
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [-0.9, 0.25, ...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 0 diff: -0.9 != 0.1
Full diff:
[
- 0.1,
? ^
+ -0.9,
? + ^
0.25,
0.5,
1.0,
2.5,
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 4.05s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 76, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 1.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:100: in <module>
pr_approval_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 77, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, -0.75, 0.5, 1.0, 2.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:100: in <module>
pr_approval_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 78, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 1.5, 1.0, 2.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:100: in <module>
pr_approval_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 79, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, -0.5, 1.0, 2.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:100: in <module>
pr_approval_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 80, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 2.0, 2.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 3 diff: 2.0 != 1.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E - 1.0,
E ? ^
E + 2.0,
E ? ^
E 2.5,
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 3 diff: 2.0 != 1.0
Full diff:
[
0.1,
0.25,
0.5,
- 1.0,
? ^
+ 2.0,
? ^
2.5,
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 81, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 0.0, 2.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:100: in <module>
pr_approval_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 82, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 3.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...3.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 4 diff: 3.5 != 2.5
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 3.5,
E ? ^
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...3.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 4 diff: 3.5 != 2.5
Full diff:
[
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 3.5,
? ^
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.93s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 83, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 1.5, 5.0, 10.0),
)
pr_dismissals_total = Counter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...1.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 4 diff: 1.5 != 2.5
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 1.5,
E ? ^
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...1.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 4 diff: 1.5 != 2.5
Full diff:
[
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 1.5,
? ^
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 84, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 6.0, 10.0),
)
pr_dismissals_total = Counter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 6.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 5 diff: 6.0 != 5.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 6.0,
E ? ^
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 6.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 5 diff: 6.0 != 5.0
Full diff:
[
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 6.0,
? ^
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 85, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 4.0, 10.0),
)
pr_dismissals_total = Counter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 4.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 5 diff: 4.0 != 5.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 4.0,
E ? ^
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 4.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 5 diff: 4.0 != 5.0
Full diff:
[
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 4.0,
? ^
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 86, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 11.0),
)
pr_dismissals_total = Counter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 6 diff: 11.0 != 10.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^
E + 11.0,
E ? ^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 6 diff: 11.0 != 10.0
Full diff:
[
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^
+ 11.0,
? ^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 87, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -100,7 +100,7 @@
pr_approval_duration_seconds = Histogram(
"stampbot_pr_approval_duration_seconds",
"PR approval operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 9.0),
)
pr_dismissals_total = Counter(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 6 diff: 9.0 != 10.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^^
E + 9.0,
E ? ^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 6 diff: 9.0 != 10.0
Full diff:
[
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^^
+ 9.0,
? ^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 88, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=( 1.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:115: in <module>
pr_dismissal_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 89, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=( -0.9, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [-0.9, 0.25, ...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 0 diff: -0.9 != 0.1
E
E Full diff:
E [
E - 0.1,
E ? ^
E + -0.9,
E ? + ^
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [-0.9, 0.25, ...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 0 diff: -0.9 != 0.1
Full diff:
[
- 0.1,
? ^
+ -0.9,
? + ^
0.25,
0.5,
1.0,
2.5,
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 90, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 1.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:115: in <module>
pr_dismissal_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 91, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, -0.75, 0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:115: in <module>
pr_dismissal_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 92, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 1.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:115: in <module>
pr_dismissal_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 93, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, -0.5, 1.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:115: in <module>
pr_dismissal_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 94, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 2.0, 2.5, 5.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 3 diff: 2.0 != 1.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E - 1.0,
E ? ^
E + 2.0,
E ? ^
E 2.5,
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 3 diff: 2.0 != 1.0
Full diff:
[
0.1,
0.25,
0.5,
- 1.0,
? ^
+ 2.0,
? ^
2.5,
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 95, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 0.0, 2.5, 5.0, 10.0),
)
# =============================================================================
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:115: in <module>
pr_dismissal_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 96, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 3.5, 5.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...3.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 4 diff: 3.5 != 2.5
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 3.5,
E ? ^
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...3.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 4 diff: 3.5 != 2.5
Full diff:
[
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 3.5,
? ^
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 97, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 1.5, 5.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...1.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 4 diff: 1.5 != 2.5
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 1.5,
E ? ^
E 5.0,
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...1.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 4 diff: 1.5 != 2.5
Full diff:
[
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 1.5,
? ^
5.0,
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.90s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 98, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 6.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 6.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 5 diff: 6.0 != 5.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 6.0,
E ? ^
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 6.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 5 diff: 6.0 != 5.0
Full diff:
[
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 6.0,
? ^
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 99, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 4.0, 10.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 4.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 5 diff: 4.0 != 5.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 4.0,
E ? ^
E 10.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 4.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 5 diff: 4.0 != 5.0
Full diff:
[
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 4.0,
? ^
10.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 100, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 11.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 6 diff: 11.0 != 10.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^
E + 11.0,
E ? ^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 6 diff: 11.0 != 10.0
Full diff:
[
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^
+ 11.0,
? ^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 101, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -115,7 +115,7 @@
pr_dismissal_duration_seconds = Histogram(
"stampbot_pr_dismissal_duration_seconds",
"PR dismissal operation duration in seconds",
- buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
+ buckets=(0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 9.0),
)
# =============================================================================........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
E
E At index 6 diff: 9.0 != 10.0
E
E Full diff:
E [
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^^
E + 9.0,
E ? ^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.1, 0.25, 0...2.5, 5.0, ...] == [0.1, 0.25, 0...2.5, 5.0, ...]
At index 6 diff: 9.0 != 10.0
Full diff:
[
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^^
+ 9.0,
? ^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.77s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 102, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=( 1.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:144: in <module>
github_api_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 103, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=( -0.95, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [-0.95, 0.1, ...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 0 diff: -0.95 != 0.05
E
E Full diff:
E [
E - 0.05,
E ? ^
E + -0.95,
E ? + ^
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E 10.0,
E 30.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [-0.95, 0.1, ...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 0 diff: -0.95 != 0.05
Full diff:
[
- 0.05,
? ^
+ -0.95,
? + ^
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
10.0,
30.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.75s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 104, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 1.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:144: in <module>
github_api_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 105, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, -0.9, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:144: in <module>
github_api_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 106, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 1.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:144: in <module>
github_api_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 107, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, -0.75, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:144: in <module>
github_api_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.72s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 108, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 1.5, 1.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:144: in <module>
github_api_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 109, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, -0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:144: in <module>
github_api_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 110, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 2.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.05, 0.1, 0...2.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 4 diff: 2.0 != 1.0
E
E Full diff:
E [
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E - 1.0,
E ? ^
E + 2.0,
E ? ^
E 2.5,
E 5.0,
E 10.0,
E 30.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.05, 0.1, 0...2.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 4 diff: 2.0 != 1.0
Full diff:
[
0.05,
0.1,
0.25,
0.5,
- 1.0,
? ^
+ 2.0,
? ^
2.5,
5.0,
10.0,
30.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.91s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 111, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 0.0, 2.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_http_metrics.py __________________
tests/test_http_metrics.py:11: in <module>
from stampbot.main import UNMATCHED_ENDPOINT, _metric_endpoint_label, metrics_middleware
stampbot/main.py:25: in <module>
from stampbot.github_client import _sanitize_error
stampbot/github_client.py:22: in <module>
from stampbot.installation_auth import InstrumentedInstallationAuth
stampbot/installation_auth.py:12: in <module>
from stampbot.metrics import github_api_request_duration_seconds, github_api_requests_total
stampbot/metrics.py:144: in <module>
github_api_request_duration_seconds = Histogram(
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:627: in __init__
self._prepare_buckets(buckets)
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/prometheus_client/metrics.py:645: in _prepare_buckets
raise ValueError('Buckets not in sorted order')
E ValueError: Buckets not in sorted order
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
ERROR tests/test_http_metrics.py - ValueError: Buckets not in sorted order
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 1.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 112, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 3.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.05, 0.1, 0...1.0, 3.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 5 diff: 3.5 != 2.5
E
E Full diff:
E [
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 3.5,
E ? ^
E 5.0,
E 10.0,
E 30.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.05, 0.1, 0...1.0, 3.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 5 diff: 3.5 != 2.5
Full diff:
[
0.05,
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 3.5,
? ^
5.0,
10.0,
30.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.80s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 113, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 1.5, 5.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.05, 0.1, 0...1.0, 1.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 5 diff: 1.5 != 2.5
E
E Full diff:
E [
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E - 2.5,
E ? ^
E + 1.5,
E ? ^
E 5.0,
E 10.0,
E 30.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.05, 0.1, 0...1.0, 1.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 5 diff: 1.5 != 2.5
Full diff:
[
0.05,
0.1,
0.25,
0.5,
1.0,
- 2.5,
? ^
+ 1.5,
? ^
5.0,
10.0,
30.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.85s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 114, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 6.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 6 diff: 6.0 != 5.0
E
E Full diff:
E [
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 6.0,
E ? ^
E 10.0,
E 30.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 6 diff: 6.0 != 5.0
Full diff:
[
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 6.0,
? ^
10.0,
30.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.78s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 115, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 4.0, 10.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 6 diff: 4.0 != 5.0
E
E Full diff:
E [
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E - 5.0,
E ? ^
E + 4.0,
E ? ^
E 10.0,
E 30.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 6 diff: 4.0 != 5.0
Full diff:
[
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
- 5.0,
? ^
+ 4.0,
? ^
10.0,
30.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 116, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 11.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 7 diff: 11.0 != 10.0
E
E Full diff:
E [
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^
E + 11.0,
E ? ^
E 30.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 7 diff: 11.0 != 10.0
Full diff:
[
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^
+ 11.0,
? ^
30.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 117, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 9.0, 30.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 7 diff: 9.0 != 10.0
E
E Full diff:
E [
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E - 10.0,
E ? ^^
E + 9.0,
E ? ^
E 30.0,
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 7 diff: 9.0 != 10.0
Full diff:
[
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
- 10.0,
? ^^
+ 9.0,
? ^
30.0,
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 118, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 31.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 8 diff: 31.0 != 30.0
E
E Full diff:
E [
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E 10.0,
E - 30.0,
E ? ^
E + 31.0,
E ? ^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 8 diff: 31.0 != 30.0
Full diff:
[
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
10.0,
- 30.0,
? ^
+ 31.0,
? ^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.88s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 119, definition_name: None
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -145,7 +145,7 @@
"stampbot_github_api_request_duration_seconds",
"GitHub API request duration in seconds",
["operation"],
- buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
+ buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 29.0),
)
github_api_rate_limit_remaining = Gauge(........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
....................................................F
=================================== FAILURES ===================================
___________________________ test_histogram_contracts ___________________________
def test_histogram_contracts():
"""Test histogram names, labels, and bucket boundaries."""
expected_histograms = {
"http_request_duration_seconds": {
"name": "stampbot_http_request_duration_seconds",
"labels": ("method", "endpoint"),
"buckets": (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"http_request_size_bytes": {
"name": "stampbot_http_request_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"http_response_size_bytes": {
"name": "stampbot_http_response_size_bytes",
"labels": ("method", "endpoint"),
"buckets": (100, 500, 1000, 5000, 10000, 50000, 100000, 500000),
},
"webhook_processing_duration_seconds": {
"name": "stampbot_webhook_processing_duration_seconds",
"labels": ("event_type",),
"buckets": (0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_approval_duration_seconds": {
"name": "stampbot_pr_approval_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"pr_dismissal_duration_seconds": {
"name": "stampbot_pr_dismissal_duration_seconds",
"labels": (),
"buckets": (0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
},
"github_api_request_duration_seconds": {
"name": "stampbot_github_api_request_duration_seconds",
"labels": ("operation",),
"buckets": (0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
},
}
for attribute, expected in expected_histograms.items():
histogram = getattr(metrics, attribute)
assert isinstance(histogram, Histogram)
assert histogram._name == expected["name"]
assert histogram._labelnames == expected["labels"]
> assert histogram._upper_bounds == [*expected["buckets"], math.inf]
E assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
E
E At index 8 diff: 29.0 != 30.0
E
E Full diff:
E [
E 0.05,
E 0.1,
E 0.25,
E 0.5,
E 1.0,
E 2.5,
E 5.0,
E 10.0,
E - 30.0,
E ? ^^
E + 29.0,
E ? ^^
E inf,
E ]
tests/test_metrics.py:59: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_metrics.py::test_histogram_contracts - assert [0.05, 0.1, 0...1.0, 2.5, ...] == [0.05, 0.1, 0...1.0, 2.5, ...]
At index 8 diff: 29.0 != 30.0
Full diff:
[
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
10.0,
- 30.0,
? ^^
+ 29.0,
? ^^
inf,
]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 268 passed, 3 deselected, 1 warning in 3.83s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 120, definition_name: stop_metrics_server
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -204,7 +204,7 @@
"""
server.shutdown()
server.server_close()
- thread.join(timeout=5)
+ thread.join(timeout= 6)
def set_app_info(version: str) -> None:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 121, definition_name: stop_metrics_server
--- mutation diff ---
--- a/stampbot/metrics.py
+++ b/stampbot/metrics.py
@@ -204,7 +204,7 @@
"""
server.shutdown()
server.server_close()
- thread.join(timeout=5)
+ thread.join(timeout= 4)
def set_app_info(version: str) -> None:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 4.99s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_NotEq, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -31,7 +31,7 @@
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
- if parsed.scheme == "http":
+ if parsed.scheme != "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................F
=================================== FAILURES ===================================
_________________ TestUrlValidation.test_allows_http_localhost _________________
self = <tests.test_manifest.TestUrlValidation object at 0x7f3157564190>
def test_allows_http_localhost(self):
"""Test that HTTP URLs for localhost are allowed."""
> manifest = create_manifest(
redirect_url="http://localhost:8000/setup/callback",
webhook_url="http://localhost:8000/webhook",
)
tests/test_manifest.py:33:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:130: in create_manifest
_validate_url(redirect_url, "redirect_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'http://localhost:8000/setup/callback', name = 'redirect_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme != "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":
> raise ValueError(f"Invalid {name}: must use HTTPS")
E ValueError: Invalid redirect_url: must use HTTPS
stampbot/manifest.py:38: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_allows_http_localhost - ValueError: Invalid redirect_url: must use HTTPS
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 233 passed, 3 deselected, 1 warning in 3.65s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Lt, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -31,7 +31,7 @@
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
- if parsed.scheme == "http":
+ if parsed.scheme < "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................F
=================================== FAILURES ===================================
_________________ TestUrlValidation.test_allows_http_localhost _________________
self = <tests.test_manifest.TestUrlValidation object at 0x7f73b9264190>
def test_allows_http_localhost(self):
"""Test that HTTP URLs for localhost are allowed."""
> manifest = create_manifest(
redirect_url="http://localhost:8000/setup/callback",
webhook_url="http://localhost:8000/webhook",
)
tests/test_manifest.py:33:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:130: in create_manifest
_validate_url(redirect_url, "redirect_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'http://localhost:8000/setup/callback', name = 'redirect_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme < "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":
> raise ValueError(f"Invalid {name}: must use HTTPS")
E ValueError: Invalid redirect_url: must use HTTPS
stampbot/manifest.py:38: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_allows_http_localhost - ValueError: Invalid redirect_url: must use HTTPS
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 233 passed, 3 deselected, 1 warning in 3.70s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Eq_LtE, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -31,7 +31,7 @@
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
- if parsed.scheme == "http":
+ if parsed.scheme <= "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.11s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Gt, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -31,7 +31,7 @@
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
- if parsed.scheme == "http":
+ if parsed.scheme > "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................F
=================================== FAILURES ===================================
_________________ TestUrlValidation.test_allows_http_localhost _________________
self = <tests.test_manifest.TestUrlValidation object at 0x7f58c9184190>
def test_allows_http_localhost(self):
"""Test that HTTP URLs for localhost are allowed."""
> manifest = create_manifest(
redirect_url="http://localhost:8000/setup/callback",
webhook_url="http://localhost:8000/webhook",
)
tests/test_manifest.py:33:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:130: in create_manifest
_validate_url(redirect_url, "redirect_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'http://localhost:8000/setup/callback', name = 'redirect_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme > "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":
> raise ValueError(f"Invalid {name}: must use HTTPS")
E ValueError: Invalid redirect_url: must use HTTPS
stampbot/manifest.py:38: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_allows_http_localhost - ValueError: Invalid redirect_url: must use HTTPS
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 233 passed, 3 deselected, 1 warning in 3.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_GtE, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -31,7 +31,7 @@
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
- if parsed.scheme == "http":
+ if parsed.scheme >= "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f546f07c310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:68: in validate_base_url
_validate_url(normalized, "base_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'https://stampbot.example.test', name = 'base_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme >= "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
> raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
E ValueError: Invalid base_url: must use HTTPS (HTTP only allowed for localhost)
stampbot/manifest.py:36: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must use HTTPS (HTTP only allowed for localhost)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_Is, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -31,7 +31,7 @@
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
- if parsed.scheme == "http":
+ if parsed.scheme is "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................F
=================================== FAILURES ===================================
_________________ TestUrlValidation.test_allows_http_localhost _________________
self = <tests.test_manifest.TestUrlValidation object at 0x7f71999082d0>
def test_allows_http_localhost(self):
"""Test that HTTP URLs for localhost are allowed."""
> manifest = create_manifest(
redirect_url="http://localhost:8000/setup/callback",
webhook_url="http://localhost:8000/webhook",
)
tests/test_manifest.py:33:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:130: in create_manifest
_validate_url(redirect_url, "redirect_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'http://localhost:8000/setup/callback', name = 'redirect_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme is "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":
> raise ValueError(f"Invalid {name}: must use HTTPS")
E ValueError: Invalid redirect_url: must use HTTPS
stampbot/manifest.py:38: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/manifest.py:34
/home/runner/work/stampbot/stampbot/stampbot/manifest.py:34: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
if parsed.scheme is "http":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_allows_http_localhost - ValueError: Invalid redirect_url: must use HTTPS
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 233 passed, 3 deselected, 2 warnings in 3.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Eq_IsNot, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -31,7 +31,7 @@
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
- if parsed.scheme == "http":
+ if parsed.scheme is not "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f4febfb0310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:68: in validate_base_url
_validate_url(normalized, "base_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'https://stampbot.example.test', name = 'base_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme is not "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
> raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
E ValueError: Invalid base_url: must use HTTPS (HTTP only allowed for localhost)
stampbot/manifest.py:36: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/manifest.py:34
/home/runner/work/stampbot/stampbot/stampbot/manifest.py:34: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
if parsed.scheme is not "http":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must use HTTPS (HTTP only allowed for localhost)
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 2 warnings in 3.76s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Eq, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -34,7 +34,7 @@
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
- elif parsed.scheme != "https":
+ elif parsed.scheme == "https":
raise ValueError(f"Invalid {name}: must use HTTPS")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f4bd7d88310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:68: in validate_base_url
_validate_url(normalized, "base_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'https://stampbot.example.test', name = 'base_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme == "https":
> raise ValueError(f"Invalid {name}: must use HTTPS")
E ValueError: Invalid base_url: must use HTTPS
stampbot/manifest.py:38: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must use HTTPS
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.59s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_NotEq_Lt, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -34,7 +34,7 @@
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
- elif parsed.scheme != "https":
+ elif parsed.scheme < "https":
raise ValueError(f"Invalid {name}: must use HTTPS")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_LtE, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -34,7 +34,7 @@
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
- elif parsed.scheme != "https":
+ elif parsed.scheme <= "https":
raise ValueError(f"Invalid {name}: must use HTTPS")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f8aef068310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:68: in validate_base_url
_validate_url(normalized, "base_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'https://stampbot.example.test', name = 'base_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme <= "https":
> raise ValueError(f"Invalid {name}: must use HTTPS")
E ValueError: Invalid base_url: must use HTTPS
stampbot/manifest.py:38: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must use HTTPS
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Gt, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -34,7 +34,7 @@
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
- elif parsed.scheme != "https":
+ elif parsed.scheme > "https":
raise ValueError(f"Invalid {name}: must use HTTPS")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...................F
=================================== FAILURES ===================================
_______________ TestUrlValidation.test_rejects_non_https_scheme ________________
self = <tests.test_manifest.TestUrlValidation object at 0x7f80cd9e6c40>
def test_rejects_non_https_scheme(self):
"""Test that non-HTTPS schemes are rejected for webhook URLs."""
import pytest
> with pytest.raises(ValueError, match="must use HTTPS"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:52: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_non_https_scheme - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 235 passed, 3 deselected, 1 warning in 3.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_GtE, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -34,7 +34,7 @@
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
- elif parsed.scheme != "https":
+ elif parsed.scheme >= "https":
raise ValueError(f"Invalid {name}: must use HTTPS")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f2d3a36c310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:68: in validate_base_url
_validate_url(normalized, "base_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'https://stampbot.example.test', name = 'base_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme >= "https":
> raise ValueError(f"Invalid {name}: must use HTTPS")
E ValueError: Invalid base_url: must use HTTPS
stampbot/manifest.py:38: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must use HTTPS
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_Is, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -34,7 +34,7 @@
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
- elif parsed.scheme != "https":
+ elif parsed.scheme is "https":
raise ValueError(f"Invalid {name}: must use HTTPS")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...................F
=================================== FAILURES ===================================
_______________ TestUrlValidation.test_rejects_non_https_scheme ________________
self = <tests.test_manifest.TestUrlValidation object at 0x7f19887e2c40>
def test_rejects_non_https_scheme(self):
"""Test that non-HTTPS schemes are rejected for webhook URLs."""
import pytest
> with pytest.raises(ValueError, match="must use HTTPS"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:52: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/manifest.py:37
/home/runner/work/stampbot/stampbot/stampbot/manifest.py:37: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
elif parsed.scheme is "https":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_non_https_scheme - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 235 passed, 3 deselected, 2 warnings in 3.64s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_NotEq_IsNot, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -34,7 +34,7 @@
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
- elif parsed.scheme != "https":
+ elif parsed.scheme is not "https":
raise ValueError(f"Invalid {name}: must use HTTPS")
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f63b92b0310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:68: in validate_base_url
_validate_url(normalized, "base_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'https://stampbot.example.test', name = 'base_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme is not "https":
> raise ValueError(f"Invalid {name}: must use HTTPS")
E ValueError: Invalid base_url: must use HTTPS
stampbot/manifest.py:38: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
stampbot/manifest.py:37
/home/runner/work/stampbot/stampbot/stampbot/manifest.py:37: SyntaxWarning: "is not" with 'str' literal. Did you mean "!="?
elif parsed.scheme is not "https":
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must use HTTPS
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 2 warnings in 3.60s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Lt_Eq, occurrence: 0, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -60,7 +60,7 @@
normalized = base_url.strip().rstrip("/")
if any(
- character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
+ character.isspace() or ord(character) == 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.17s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_NotEq, occurrence: 0, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -60,7 +60,7 @@
normalized = base_url.strip().rstrip("/")
if any(
- character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
+ character.isspace() or ord(character) != 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f62f3c50310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
base_url = 'https://stampbot.example.test/'
def validate_base_url(base_url: str) -> str:
"""Validate and normalize the trusted public URL used by setup.
The setup flow must receive this value from operator configuration, never
from request headers. Paths are allowed for deployments mounted below a
reverse-proxy prefix, but credentials, query strings, fragments, and
whitespace are not valid parts of the public base URL.
Args:
base_url: Operator-configured public URL.
Returns:
The normalized URL without a trailing slash.
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
if not isinstance(base_url, str) or not base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")
if any(
character.isspace() or ord(character) != 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
> raise ValueError("Invalid base_url: contains an unsafe character")
E ValueError: Invalid base_url: contains an unsafe character
stampbot/manifest.py:66: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: contains an unsafe character
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.63s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceComparisonOperator_Lt_LtE, occurrence: 0, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -60,7 +60,7 @@
normalized = base_url.strip().rstrip("/")
if any(
- character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
+ character.isspace() or ord(character) <= 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.15s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_Gt, occurrence: 0, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -60,7 +60,7 @@
normalized = base_url.strip().rstrip("/")
if any(
- character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
+ character.isspace() or ord(character) > 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f5521018310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
base_url = 'https://stampbot.example.test/'
def validate_base_url(base_url: str) -> str:
"""Validate and normalize the trusted public URL used by setup.
The setup flow must receive this value from operator configuration, never
from request headers. Paths are allowed for deployments mounted below a
reverse-proxy prefix, but credentials, query strings, fragments, and
whitespace are not valid parts of the public base URL.
Args:
base_url: Operator-configured public URL.
Returns:
The normalized URL without a trailing slash.
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
if not isinstance(base_url, str) or not base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")
if any(
character.isspace() or ord(character) > 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
> raise ValueError("Invalid base_url: contains an unsafe character")
E ValueError: Invalid base_url: contains an unsafe character
stampbot/manifest.py:66: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: contains an unsafe character
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Lt_GtE, occurrence: 0, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -60,7 +60,7 @@
normalized = base_url.strip().rstrip("/")
if any(
- character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
+ character.isspace() or ord(character) >= 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f6694684310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
base_url = 'https://stampbot.example.test/'
def validate_base_url(base_url: str) -> str:
"""Validate and normalize the trusted public URL used by setup.
The setup flow must receive this value from operator configuration, never
from request headers. Paths are allowed for deployments mounted below a
reverse-proxy prefix, but credentials, query strings, fragments, and
whitespace are not valid parts of the public base URL.
Args:
base_url: Operator-configured public URL.
Returns:
The normalized URL without a trailing slash.
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
if not isinstance(base_url, str) or not base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")
if any(
character.isspace() or ord(character) >= 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
> raise ValueError("Invalid base_url: contains an unsafe character")
E ValueError: Invalid base_url: contains an unsafe character
stampbot/manifest.py:66: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: contains an unsafe character
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_Is_IsNot, occurrence: 0, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -68,7 +68,7 @@
_validate_url(normalized, "base_url")
parsed = urlparse(normalized)
- if parsed.hostname is None:
+ if parsed.hostname is not None:
raise ValueError("Invalid base_url: hostname is required")
if parsed.username is not None or parsed.password is not None:
raise ValueError("Invalid base_url: must not contain credentials")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...............................F
=================================== FAILURES ===================================
_________ TestUrlValidation.test_rejects_unsafe_base_url[https://:443] _________
self = <tests.test_manifest.TestUrlValidation object at 0x7fc487be5550>
base_url = 'https://:443'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://:443] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 247 passed, 3 deselected, 1 warning in 3.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 0, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -70,7 +70,7 @@
if parsed.hostname is None:
raise ValueError("Invalid base_url: hostname is required")
- if parsed.username is not None or parsed.password is not None:
+ if parsed.username is None or parsed.password is not None:
raise ValueError("Invalid base_url: must not contain credentials")
if "?" in normalized or "#" in normalized:
raise ValueError("Invalid base_url: must not contain a query or fragment")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.......................F
=================================== FAILURES ===================================
__ TestUrlValidation.test_rejects_unsafe_base_url[https://user@example.test] ___
self = <tests.test_manifest.TestUrlValidation object at 0x7ff683043d50>
base_url = 'https://user@example.test'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://user@example.test] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 239 passed, 3 deselected, 1 warning in 3.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceComparisonOperator_IsNot_Is, occurrence: 1, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -70,7 +70,7 @@
if parsed.hostname is None:
raise ValueError("Invalid base_url: hostname is required")
- if parsed.username is not None or parsed.password is not None:
+ if parsed.username is not None or parsed.password is None:
raise ValueError("Invalid base_url: must not contain credentials")
if "?" in normalized or "#" in normalized:
raise ValueError("Invalid base_url: must not contain a query or fragment")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f1696370310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
base_url = 'https://stampbot.example.test/'
def validate_base_url(base_url: str) -> str:
"""Validate and normalize the trusted public URL used by setup.
The setup flow must receive this value from operator configuration, never
from request headers. Paths are allowed for deployments mounted below a
reverse-proxy prefix, but credentials, query strings, fragments, and
whitespace are not valid parts of the public base URL.
Args:
base_url: Operator-configured public URL.
Returns:
The normalized URL without a trailing slash.
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
if not isinstance(base_url, str) or not base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")
if any(
character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")
_validate_url(normalized, "base_url")
parsed = urlparse(normalized)
if parsed.hostname is None:
raise ValueError("Invalid base_url: hostname is required")
if parsed.username is not None or parsed.password is None:
> raise ValueError("Invalid base_url: must not contain credentials")
E ValueError: Invalid base_url: must not contain credentials
stampbot/manifest.py:74: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must not contain credentials
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.83s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -27,7 +27,7 @@
"""
parsed = urlparse(url)
- if not parsed.scheme or not parsed.netloc:
+ if parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
................F
=================================== FAILURES ===================================
__________ TestUrlValidation.test_rejects_http_non_localhost_redirect __________
self = <tests.test_manifest.TestUrlValidation object at 0x7fbc973a7d90>
def test_rejects_http_non_localhost_redirect(self):
"""Test that HTTP redirect URLs for non-localhost are rejected."""
import pytest
> with pytest.raises(ValueError, match="must use HTTPS"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError: Regex pattern did not match.
E Expected regex: 'must use HTTPS'
E Actual message: 'Invalid redirect_url: must be a complete URL'
tests/test_manifest.py:26: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_http_non_localhost_redirect - AssertionError: Regex pattern did not match.
Expected regex: 'must use HTTPS'
Actual message: 'Invalid redirect_url: must be a complete URL'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 232 passed, 3 deselected, 1 warning in 3.60s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 1, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -27,7 +27,7 @@
"""
parsed = urlparse(url)
- if not parsed.scheme or not parsed.netloc:
+ if not parsed.scheme or parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
................F
=================================== FAILURES ===================================
__________ TestUrlValidation.test_rejects_http_non_localhost_redirect __________
self = <tests.test_manifest.TestUrlValidation object at 0x7f1dce3a3d90>
def test_rejects_http_non_localhost_redirect(self):
"""Test that HTTP redirect URLs for non-localhost are rejected."""
import pytest
> with pytest.raises(ValueError, match="must use HTTPS"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError: Regex pattern did not match.
E Expected regex: 'must use HTTPS'
E Actual message: 'Invalid redirect_url: must be a complete URL'
tests/test_manifest.py:26: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_http_non_localhost_redirect - AssertionError: Regex pattern did not match.
Expected regex: 'must use HTTPS'
Actual message: 'Invalid redirect_url: must be a complete URL'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 232 passed, 3 deselected, 1 warning in 3.57s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 2, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -55,7 +55,7 @@
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
- if not isinstance(base_url, str) or not base_url.strip():
+ if isinstance(base_url, str) or not base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f65a7684310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
base_url = 'https://stampbot.example.test/'
def validate_base_url(base_url: str) -> str:
"""Validate and normalize the trusted public URL used by setup.
The setup flow must receive this value from operator configuration, never
from request headers. Paths are allowed for deployments mounted below a
reverse-proxy prefix, but credentials, query strings, fragments, and
whitespace are not valid parts of the public base URL.
Args:
base_url: Operator-configured public URL.
Returns:
The normalized URL without a trailing slash.
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
if isinstance(base_url, str) or not base_url.strip():
> raise ValueError("Invalid base_url: must be configured")
E ValueError: Invalid base_url: must be configured
stampbot/manifest.py:59: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must be configured
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 3, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -55,7 +55,7 @@
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
- if not isinstance(base_url, str) or not base_url.strip():
+ if not isinstance(base_url, str) or base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7fe0e7358310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
base_url = 'https://stampbot.example.test/'
def validate_base_url(base_url: str) -> str:
"""Validate and normalize the trusted public URL used by setup.
The setup flow must receive this value from operator configuration, never
from request headers. Paths are allowed for deployments mounted below a
reverse-proxy prefix, but credentials, query strings, fragments, and
whitespace are not valid parts of the public base URL.
Args:
base_url: Operator-configured public URL.
Returns:
The normalized URL without a trailing slash.
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
if not isinstance(base_url, str) or base_url.strip():
> raise ValueError("Invalid base_url: must be configured")
E ValueError: Invalid base_url: must be configured
stampbot/manifest.py:59: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must be configured
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 4, definition_name: exchange_code_for_credentials
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -192,7 +192,7 @@
ValueError: If code contains invalid characters
"""
# Validate code to prevent SSRF - GitHub codes are alphanumeric
- if not code or not code.isalnum():
+ if code or not code.isalnum():
raise ValueError("Invalid manifest code format")
url = GITHUB_MANIFEST_CONVERSION_URL.format(code=code)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...............................................F
=================================== FAILURES ===================================
_________________ TestExchangeCode.test_exchange_code_success __________________
self = <tests.test_manifest.TestExchangeCode object at 0x7f04d63987d0>
@pytest.mark.asyncio
async def test_exchange_code_success(self):
"""Test successful code exchange."""
import httpx
mock_response_data = {
"id": 12345,
"pem": "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----",
"webhook_secret": "test-secret",
"slug": "my-stampbot",
"name": "My Stampbot",
"client_id": "client123",
"client_secret": "secret456",
}
# Create a proper mock response with request attached
mock_request = httpx.Request(
"POST", "https://api.github.com/app-manifests/testcode123/conversions"
)
mock_response = httpx.Response(200, json=mock_response_data, request=mock_request)
with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
mock_post.return_value = mock_response
> result = await exchange_code_for_credentials("testcode123")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:242:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
code = 'testcode123'
async def exchange_code_for_credentials(code: str) -> dict[str, Any]:
"""Exchange the temporary code for app credentials.
After a user creates a GitHub App from a manifest, GitHub redirects
to the callback URL with a temporary code. This function exchanges
that code for the actual app credentials.
Args:
code: Temporary code from GitHub callback
Returns:
Dictionary with app credentials:
- id: App ID
- pem: Private key (PEM format)
- webhook_secret: Webhook secret
- slug: App slug
- name: App name
- client_id: OAuth client ID
- client_secret: OAuth client secret
Raises:
httpx.HTTPStatusError: If API call fails
ValueError: If code contains invalid characters
"""
# Validate code to prevent SSRF - GitHub codes are alphanumeric
if code or not code.isalnum():
> raise ValueError("Invalid manifest code format")
E ValueError: Invalid manifest code format
stampbot/manifest.py:196: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestExchangeCode::test_exchange_code_success - ValueError: Invalid manifest code format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 263 passed, 3 deselected, 1 warning in 3.67s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceUnaryOperator_Delete_Not, occurrence: 5, definition_name: exchange_code_for_credentials
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -192,7 +192,7 @@
ValueError: If code contains invalid characters
"""
# Validate code to prevent SSRF - GitHub codes are alphanumeric
- if not code or not code.isalnum():
+ if not code or code.isalnum():
raise ValueError("Invalid manifest code format")
url = GITHUB_MANIFEST_CONVERSION_URL.format(code=code)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...............................................F
=================================== FAILURES ===================================
_________________ TestExchangeCode.test_exchange_code_success __________________
self = <tests.test_manifest.TestExchangeCode object at 0x7f550aba07d0>
@pytest.mark.asyncio
async def test_exchange_code_success(self):
"""Test successful code exchange."""
import httpx
mock_response_data = {
"id": 12345,
"pem": "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----",
"webhook_secret": "test-secret",
"slug": "my-stampbot",
"name": "My Stampbot",
"client_id": "client123",
"client_secret": "secret456",
}
# Create a proper mock response with request attached
mock_request = httpx.Request(
"POST", "https://api.github.com/app-manifests/testcode123/conversions"
)
mock_response = httpx.Response(200, json=mock_response_data, request=mock_request)
with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
mock_post.return_value = mock_response
> result = await exchange_code_for_credentials("testcode123")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:242:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
code = 'testcode123'
async def exchange_code_for_credentials(code: str) -> dict[str, Any]:
"""Exchange the temporary code for app credentials.
After a user creates a GitHub App from a manifest, GitHub redirects
to the callback URL with a temporary code. This function exchanges
that code for the actual app credentials.
Args:
code: Temporary code from GitHub callback
Returns:
Dictionary with app credentials:
- id: App ID
- pem: Private key (PEM format)
- webhook_secret: Webhook secret
- slug: App slug
- name: App name
- client_id: OAuth client ID
- client_secret: OAuth client secret
Raises:
httpx.HTTPStatusError: If API call fails
ValueError: If code contains invalid characters
"""
# Validate code to prevent SSRF - GitHub codes are alphanumeric
if not code or code.isalnum():
> raise ValueError("Invalid manifest code format")
E ValueError: Invalid manifest code format
stampbot/manifest.py:196: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestExchangeCode::test_exchange_code_success - ValueError: Invalid manifest code format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 263 passed, 3 deselected, 1 warning in 3.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -27,7 +27,7 @@
"""
parsed = urlparse(url)
- if not parsed.scheme or not parsed.netloc:
+ if not not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
................F
=================================== FAILURES ===================================
__________ TestUrlValidation.test_rejects_http_non_localhost_redirect __________
self = <tests.test_manifest.TestUrlValidation object at 0x7fc33ad93d90>
def test_rejects_http_non_localhost_redirect(self):
"""Test that HTTP redirect URLs for non-localhost are rejected."""
import pytest
> with pytest.raises(ValueError, match="must use HTTPS"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError: Regex pattern did not match.
E Expected regex: 'must use HTTPS'
E Actual message: 'Invalid redirect_url: must be a complete URL'
tests/test_manifest.py:26: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_http_non_localhost_redirect - AssertionError: Regex pattern did not match.
Expected regex: 'must use HTTPS'
Actual message: 'Invalid redirect_url: must be a complete URL'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 232 passed, 3 deselected, 1 warning in 3.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 1, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -31,7 +31,7 @@
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
- if parsed.scheme == "http":
+ if not parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................F
=================================== FAILURES ===================================
_________________ TestUrlValidation.test_allows_http_localhost _________________
self = <tests.test_manifest.TestUrlValidation object at 0x7f4f6f168190>
def test_allows_http_localhost(self):
"""Test that HTTP URLs for localhost are allowed."""
> manifest = create_manifest(
redirect_url="http://localhost:8000/setup/callback",
webhook_url="http://localhost:8000/webhook",
)
tests/test_manifest.py:33:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:130: in create_manifest
_validate_url(redirect_url, "redirect_url")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
url = 'http://localhost:8000/setup/callback', name = 'redirect_url'
def _validate_url(url: str, name: str) -> None:
"""Validate that a URL is well-formed and uses HTTPS (or localhost).
Args:
url: URL to validate
name: Name of the URL parameter for error messages
Raises:
ValueError: If URL is invalid or doesn't use HTTPS
"""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)
if not parsed.scheme == "http":
if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":
> raise ValueError(f"Invalid {name}: must use HTTPS")
E ValueError: Invalid redirect_url: must use HTTPS
stampbot/manifest.py:38: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_allows_http_localhost - ValueError: Invalid redirect_url: must use HTTPS
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 233 passed, 3 deselected, 1 warning in 3.58s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 2, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -32,7 +32,7 @@
# Allow http only for localhost (development)
if parsed.scheme == "http":
- if parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
+ if not parsed.hostname not in ("localhost", "127.0.0.1", "::1"):
raise ValueError(f"Invalid {name}: must use HTTPS (HTTP only allowed for localhost)")
elif parsed.scheme != "https":
raise ValueError(f"Invalid {name}: must use HTTPS")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
................F
=================================== FAILURES ===================================
__________ TestUrlValidation.test_rejects_http_non_localhost_redirect __________
self = <tests.test_manifest.TestUrlValidation object at 0x7f4a8e377d90>
def test_rejects_http_non_localhost_redirect(self):
"""Test that HTTP redirect URLs for non-localhost are rejected."""
import pytest
> with pytest.raises(ValueError, match="must use HTTPS"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:26: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_http_non_localhost_redirect - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 232 passed, 3 deselected, 1 warning in 3.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 3, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -55,7 +55,7 @@
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
- if not isinstance(base_url, str) or not base_url.strip():
+ if not not isinstance(base_url, str) or not base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f293bf80310>
base_url = 'https://stampbot.example.test/'
expected = 'https://stampbot.example.test'
@pytest.mark.parametrize(
("base_url", "expected"),
[
("https://stampbot.example.test/", "https://stampbot.example.test"),
("https://stampbot.example.test/service/", "https://stampbot.example.test/service"),
("http://localhost:8000/", "http://localhost:8000"),
],
)
def test_normalizes_safe_base_url(self, base_url, expected):
"""Test trusted setup base URLs preserve supported deployment forms."""
> assert validate_base_url(base_url) == expected
^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:91:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
base_url = 'https://stampbot.example.test/'
def validate_base_url(base_url: str) -> str:
"""Validate and normalize the trusted public URL used by setup.
The setup flow must receive this value from operator configuration, never
from request headers. Paths are allowed for deployments mounted below a
reverse-proxy prefix, but credentials, query strings, fragments, and
whitespace are not valid parts of the public base URL.
Args:
base_url: Operator-configured public URL.
Returns:
The normalized URL without a trailing slash.
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
if not not isinstance(base_url, str) or not base_url.strip():
> raise ValueError("Invalid base_url: must be configured")
E ValueError: Invalid base_url: must be configured
stampbot/manifest.py:59: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_normalizes_safe_base_url[https://stampbot.example.test/-https://stampbot.example.test] - ValueError: Invalid base_url: must be configured
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 249 passed, 3 deselected, 1 warning in 3.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 4, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -59,7 +59,7 @@
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")
- if any(
+ if not any(
character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
............................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_rejects_unsafe_base_url[https://example.test/bad path] _
self = <tests.test_manifest.TestUrlValidation object at 0x7fbfa28b0ad0>
base_url = 'https://example.test/bad path'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://example.test/bad path] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 244 passed, 3 deselected, 1 warning in 4.92s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 5, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -68,7 +68,7 @@
_validate_url(normalized, "base_url")
parsed = urlparse(normalized)
- if parsed.hostname is None:
+ if not parsed.hostname is None:
raise ValueError("Invalid base_url: hostname is required")
if parsed.username is not None or parsed.password is not None:
raise ValueError("Invalid base_url: must not contain credentials")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...............................F
=================================== FAILURES ===================================
_________ TestUrlValidation.test_rejects_unsafe_base_url[https://:443] _________
self = <tests.test_manifest.TestUrlValidation object at 0x7f3fff6e5550>
base_url = 'https://:443'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://:443] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 247 passed, 3 deselected, 1 warning in 3.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 6, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -70,7 +70,7 @@
if parsed.hostname is None:
raise ValueError("Invalid base_url: hostname is required")
- if parsed.username is not None or parsed.password is not None:
+ if not parsed.username is not None or parsed.password is not None:
raise ValueError("Invalid base_url: must not contain credentials")
if "?" in normalized or "#" in normalized:
raise ValueError("Invalid base_url: must not contain a query or fragment")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.......................F
=================================== FAILURES ===================================
__ TestUrlValidation.test_rejects_unsafe_base_url[https://user@example.test] ___
self = <tests.test_manifest.TestUrlValidation object at 0x7f8b8f857d50>
base_url = 'https://user@example.test'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://user@example.test] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 239 passed, 3 deselected, 1 warning in 3.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 7, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -72,7 +72,7 @@
raise ValueError("Invalid base_url: hostname is required")
if parsed.username is not None or parsed.password is not None:
raise ValueError("Invalid base_url: must not contain credentials")
- if "?" in normalized or "#" in normalized:
+ if not "?" in normalized or "#" in normalized:
raise ValueError("Invalid base_url: must not contain a query or fragment")
try:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_rejects_unsafe_base_url[https://example.test?next=elsewhere] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f198b4c0050>
base_url = 'https://example.test?next=elsewhere'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://example.test?next=elsewhere] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 240 passed, 3 deselected, 1 warning in 3.62s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 8, definition_name: create_manifest
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -143,7 +143,7 @@
}
# Only include webhook URL if provided; GitHub will prompt for it otherwise
- if webhook_url:
+ if not webhook_url:
_validate_url(webhook_url, "webhook_url")
manifest["hook_attributes"] = {
"url": webhook_url,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.................F
=================================== FAILURES ===================================
_________________ TestUrlValidation.test_allows_http_localhost _________________
self = <tests.test_manifest.TestUrlValidation object at 0x7fc45d77c190>
def test_allows_http_localhost(self):
"""Test that HTTP URLs for localhost are allowed."""
manifest = create_manifest(
redirect_url="http://localhost:8000/setup/callback",
webhook_url="http://localhost:8000/webhook",
)
> assert manifest["hook_attributes"]["url"] == "http://localhost:8000/webhook"
^^^^^^^^^^^^^^^^^^^^^^^^^^^
E KeyError: 'hook_attributes'
tests/test_manifest.py:37: KeyError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_allows_http_localhost - KeyError: 'hook_attributes'
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 233 passed, 3 deselected, 1 warning in 3.69s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/AddNot, occurrence: 9, definition_name: exchange_code_for_credentials
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -192,7 +192,7 @@
ValueError: If code contains invalid characters
"""
# Validate code to prevent SSRF - GitHub codes are alphanumeric
- if not code or not code.isalnum():
+ if not not code or not code.isalnum():
raise ValueError("Invalid manifest code format")
url = GITHUB_MANIFEST_CONVERSION_URL.format(code=code)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
...............................................F
=================================== FAILURES ===================================
_________________ TestExchangeCode.test_exchange_code_success __________________
self = <tests.test_manifest.TestExchangeCode object at 0x7f706bf587d0>
@pytest.mark.asyncio
async def test_exchange_code_success(self):
"""Test successful code exchange."""
import httpx
mock_response_data = {
"id": 12345,
"pem": "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----",
"webhook_secret": "test-secret",
"slug": "my-stampbot",
"name": "My Stampbot",
"client_id": "client123",
"client_secret": "secret456",
}
# Create a proper mock response with request attached
mock_request = httpx.Request(
"POST", "https://api.github.com/app-manifests/testcode123/conversions"
)
mock_response = httpx.Response(200, json=mock_response_data, request=mock_request)
with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
mock_post.return_value = mock_response
> result = await exchange_code_for_credentials("testcode123")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_manifest.py:242:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
code = 'testcode123'
async def exchange_code_for_credentials(code: str) -> dict[str, Any]:
"""Exchange the temporary code for app credentials.
After a user creates a GitHub App from a manifest, GitHub redirects
to the callback URL with a temporary code. This function exchanges
that code for the actual app credentials.
Args:
code: Temporary code from GitHub callback
Returns:
Dictionary with app credentials:
- id: App ID
- pem: Private key (PEM format)
- webhook_secret: Webhook secret
- slug: App slug
- name: App name
- client_id: OAuth client ID
- client_secret: OAuth client secret
Raises:
httpx.HTTPStatusError: If API call fails
ValueError: If code contains invalid characters
"""
# Validate code to prevent SSRF - GitHub codes are alphanumeric
if not not code or not code.isalnum():
> raise ValueError("Invalid manifest code format")
E ValueError: Invalid manifest code format
stampbot/manifest.py:196: ValueError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestExchangeCode::test_exchange_code_success - ValueError: Invalid manifest code format
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 263 passed, 3 deselected, 1 warning in 3.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceTrueWithFalse, occurrence: 0, definition_name: create_manifest
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -147,7 +147,7 @@
_validate_url(webhook_url, "webhook_url")
manifest["hook_attributes"] = {
"url": webhook_url,
- "active": True,
+ "active": False,
}
return manifest........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
......................................F
=================================== FAILURES ===================================
__________ TestCreateManifest.test_manifest_webhook_url_when_provided __________
self = <tests.test_manifest.TestCreateManifest object at 0x7f1bbdc87a80>
def test_manifest_webhook_url_when_provided(self):
"""Test manifest contains webhook URL when provided."""
manifest = create_manifest(
redirect_url="https://stampbot.example.com/setup/callback",
webhook_url="https://stampbot.example.com/webhook",
)
assert manifest["hook_attributes"]["url"] == "https://stampbot.example.com/webhook"
> assert manifest["hook_attributes"]["active"] is True
E assert False is True
tests/test_manifest.py:130: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestCreateManifest::test_manifest_webhook_url_when_provided - assert False is True
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 254 passed, 3 deselected, 1 warning in 3.63s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceFalseWithTrue, occurrence: 0, definition_name: create_manifest
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -137,7 +137,7 @@
"description": app_description,
"url": base_url,
"redirect_url": redirect_url,
- "public": False,
+ "public": True,
"default_permissions": MANIFEST_PERMISSIONS,
"default_events": MANIFEST_EVENTS,
}........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..........................................F
=================================== FAILURES ===================================
_________________ TestCreateManifest.test_manifest_is_private __________________
self = <tests.test_manifest.TestCreateManifest object at 0x7f3bcb0f7460>
def test_manifest_is_private(self):
"""Test manifest creates private app."""
manifest = create_manifest(
redirect_url="https://example.com/setup/callback",
)
> assert manifest["public"] is False
E assert True is False
tests/test_manifest.py:162: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestCreateManifest::test_manifest_is_private - assert True is False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 258 passed, 3 deselected, 1 warning in 3.70s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 0, definition_name: _validate_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -27,7 +27,7 @@
"""
parsed = urlparse(url)
- if not parsed.scheme or not parsed.netloc:
+ if not parsed.scheme and not parsed.netloc:
raise ValueError(f"Invalid {name}: must be a complete URL")
# Allow http only for localhost (development)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.08s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/ReplaceOrWithAnd, occurrence: 1, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -55,7 +55,7 @@
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
- if not isinstance(base_url, str) or not base_url.strip():
+ if not isinstance(base_url, str) and not base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.03s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 2, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -60,7 +60,7 @@
normalized = base_url.strip().rstrip("/")
if any(
- character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
+ character.isspace() and ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
............................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_rejects_unsafe_base_url[https://example.test/bad path] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f3b3dda4910>
base_url = 'https://example.test/bad path'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://example.test/bad path] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 244 passed, 3 deselected, 1 warning in 3.66s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 3, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -60,7 +60,7 @@
normalized = base_url.strip().rstrip("/")
if any(
- character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
+ character.isspace() or ord(character) < 32 and character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.............................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_rejects_unsafe_base_url[https://example.test/<unsafe>] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f547f9b7520>
base_url = 'https://example.test/<unsafe>'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://example.test/<unsafe>] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 245 passed, 3 deselected, 1 warning in 3.59s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 4, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -70,7 +70,7 @@
if parsed.hostname is None:
raise ValueError("Invalid base_url: hostname is required")
- if parsed.username is not None or parsed.password is not None:
+ if parsed.username is not None and parsed.password is not None:
raise ValueError("Invalid base_url: must not contain credentials")
if "?" in normalized or "#" in normalized:
raise ValueError("Invalid base_url: must not contain a query or fragment")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.......................F
=================================== FAILURES ===================================
__ TestUrlValidation.test_rejects_unsafe_base_url[https://user@example.test] ___
self = <tests.test_manifest.TestUrlValidation object at 0x7f34cee4bd50>
base_url = 'https://user@example.test'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://user@example.test] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 239 passed, 3 deselected, 1 warning in 3.74s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 5, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -72,7 +72,7 @@
raise ValueError("Invalid base_url: hostname is required")
if parsed.username is not None or parsed.password is not None:
raise ValueError("Invalid base_url: must not contain credentials")
- if "?" in normalized or "#" in normalized:
+ if "?" in normalized and "#" in normalized:
raise ValueError("Invalid base_url: must not contain a query or fragment")
try:........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_rejects_unsafe_base_url[https://example.test?next=elsewhere] _
self = <tests.test_manifest.TestUrlValidation object at 0x7f590f8bc050>
base_url = 'https://example.test?next=elsewhere'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
> with pytest.raises(ValueError, match="Invalid base_url"):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E Failed: DID NOT RAISE ValueError
tests/test_manifest.py:78: Failed
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://example.test?next=elsewhere] - Failed: DID NOT RAISE ValueError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 240 passed, 3 deselected, 1 warning in 3.70s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ReplaceOrWithAnd, occurrence: 6, definition_name: exchange_code_for_credentials
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -192,7 +192,7 @@
ValueError: If code contains invalid characters
"""
# Validate code to prevent SSRF - GitHub codes are alphanumeric
- if not code or not code.isalnum():
+ if not code and not code.isalnum():
raise ValueError("Invalid manifest code format")
url = GITHUB_MANIFEST_CONVERSION_URL.format(code=code)........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
..................................................F
=================================== FAILURES ===================================
________ TestExchangeCode.test_exchange_code_rejects_invalid_characters ________
self = <tests.test_manifest.TestExchangeCode object at 0x7f1adf787ce0>
@pytest.mark.asyncio
async def test_exchange_code_rejects_invalid_characters(self):
"""Test that code with special characters raises ValueError to prevent SSRF."""
with pytest.raises(ValueError, match="Invalid manifest code format"):
> await exchange_code_for_credentials("../../../etc/passwd")
tests/test_manifest.py:281:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
stampbot/manifest.py:210: in exchange_code_for_credentials
response.raise_for_status()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Response [404 Not Found]>
def raise_for_status(self) -> Response:
"""
Raise the `HTTPStatusError` if one occurred.
"""
request = self._request
if request is None:
raise RuntimeError(
"Cannot call `raise_for_status` as the request "
"instance has not been set on this response."
)
if self.is_success:
return self
if self.has_redirect_location:
message = (
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
"Redirect location: '{0.headers[location]}'\n"
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
)
else:
message = (
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
)
status_class = self.status_code // 100
error_types = {
1: "Informational response",
3: "Redirect response",
4: "Client error",
5: "Server error",
}
error_type = error_types.get(status_class, "Invalid status code")
message = message.format(self, error_type=error_type)
> raise HTTPStatusError(message, request=request, response=self)
E httpx.HTTPStatusError: Client error '404 Not Found' for url 'https://api.github.com/etc/passwd/conversions'
E For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/httpx/_models.py:829: HTTPStatusError
----------------------------- Captured stdout call -----------------------------
{"event": "Exchanging manifest code for credentials", "logger": "stampbot.manifest", "level": "info", "timestamp": "2026-09-04T21:16:02.464621Z", "_logger": "<_FixedFindCallerLogger stampbot.manifest (INFO)>", "_name": "info"}
{"event": "HTTP Request: POST https://api.github.com/etc/passwd/conversions \"HTTP/1.1 404 Not Found\"", "logger": "httpx", "level": "info", "timestamp": "2026-09-04T21:16:02.499843Z"}
------------------------------ Captured log call -------------------------------
INFO stampbot.manifest:manifest.py:200 {'event': 'Exchanging manifest code for credentials', 'logger': 'stampbot.manifest', 'level': 'info', 'timestamp': '2026-09-04T21:16:02.464621Z'}
INFO httpx:_client.py:1740 HTTP Request: POST https://api.github.com/etc/passwd/conversions "HTTP/1.1 404 Not Found"
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestExchangeCode::test_exchange_code_rejects_invalid_characters - httpx.HTTPStatusError: Client error '404 Not Found' for url 'https://api.github.com/etc/passwd/conversions'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 266 passed, 3 deselected, 1 warning in 3.82s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/ExceptionReplacer, occurrence: 0, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -77,7 +77,7 @@
try:
_ = parsed.port
- except ValueError as error:
+ except CosmicRayTestingException as error:
raise ValueError("Invalid base_url: port is invalid") from error
return normalized........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
................................F
=================================== FAILURES ===================================
_ TestUrlValidation.test_rejects_unsafe_base_url[https://example.test:invalid] _
base_url = 'https://example.test:invalid'
def validate_base_url(base_url: str) -> str:
"""Validate and normalize the trusted public URL used by setup.
The setup flow must receive this value from operator configuration, never
from request headers. Paths are allowed for deployments mounted below a
reverse-proxy prefix, but credentials, query strings, fragments, and
whitespace are not valid parts of the public base URL.
Args:
base_url: Operator-configured public URL.
Returns:
The normalized URL without a trailing slash.
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
if not isinstance(base_url, str) or not base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")
if any(
character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")
_validate_url(normalized, "base_url")
parsed = urlparse(normalized)
if parsed.hostname is None:
raise ValueError("Invalid base_url: hostname is required")
if parsed.username is not None or parsed.password is not None:
raise ValueError("Invalid base_url: must not contain credentials")
if "?" in normalized or "#" in normalized:
raise ValueError("Invalid base_url: must not contain a query or fragment")
try:
> _ = parsed.port
^^^^^^^^^^^
stampbot/manifest.py:79:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = ParseResult(scheme='https', netloc='example.test:invalid', path='', params='', query='', fragment='')
@property
def port(self):
port = self._hostinfo[1]
if port is not None:
if port.isdigit() and port.isascii():
port = int(port)
else:
> raise ValueError(f"Port could not be cast to integer value as {port!r}")
E ValueError: Port could not be cast to integer value as 'invalid'
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/urllib/parse.py:182: ValueError
During handling of the above exception, another exception occurred:
self = <tests.test_manifest.TestUrlValidation object at 0x7fe164e6ccb0>
base_url = 'https://example.test:invalid'
@pytest.mark.parametrize(
"base_url",
[
"",
"example.test",
"http://public.example.test",
"https://user@example.test",
"https://example.test?next=elsewhere",
"https://example.test#fragment",
"https://example.test?",
"https://example.test/#",
"https://example.test/bad path",
"https://example.test/<unsafe>",
"https://example.test\\unexpected",
"https://:443",
"https://example.test:invalid",
],
)
def test_rejects_unsafe_base_url(self, base_url):
"""Test trusted setup base URLs reject ambiguous or unsafe forms."""
with pytest.raises(ValueError, match="Invalid base_url"):
> validate_base_url(base_url)
tests/test_manifest.py:79:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
base_url = 'https://example.test:invalid'
def validate_base_url(base_url: str) -> str:
"""Validate and normalize the trusted public URL used by setup.
The setup flow must receive this value from operator configuration, never
from request headers. Paths are allowed for deployments mounted below a
reverse-proxy prefix, but credentials, query strings, fragments, and
whitespace are not valid parts of the public base URL.
Args:
base_url: Operator-configured public URL.
Returns:
The normalized URL without a trailing slash.
Raises:
ValueError: If the URL is incomplete, unsafe, or unsupported.
"""
if not isinstance(base_url, str) or not base_url.strip():
raise ValueError("Invalid base_url: must be configured")
normalized = base_url.strip().rstrip("/")
if any(
character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")
_validate_url(normalized, "base_url")
parsed = urlparse(normalized)
if parsed.hostname is None:
raise ValueError("Invalid base_url: hostname is required")
if parsed.username is not None or parsed.password is not None:
raise ValueError("Invalid base_url: must not contain credentials")
if "?" in normalized or "#" in normalized:
raise ValueError("Invalid base_url: must not contain a query or fragment")
try:
_ = parsed.port
> except CosmicRayTestingException as error:
^^^^^^^^^^^^^^^^^^^^^^^^^
E NameError: name 'CosmicRayTestingException' is not defined
stampbot/manifest.py:80: NameError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestUrlValidation::test_rejects_unsafe_base_url[https://example.test:invalid] - NameError: name 'CosmicRayTestingException' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 248 passed, 3 deselected, 1 warning in 3.74s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 0, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -60,7 +60,7 @@
normalized = base_url.strip().rstrip("/")
if any(
- character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
+ character.isspace() or ord(character) < 33 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 1, definition_name: validate_base_url
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -60,7 +60,7 @@
normalized = base_url.strip().rstrip("/")
if any(
- character.isspace() or ord(character) < 32 or character in {"<", ">", '"', "'", "\\"}
+ character.isspace() or ord(character) < 31 or character in {"<", ">", '"', "'", "\\"}
for character in normalized
):
raise ValueError("Invalid base_url: contains an unsafe character")........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.07s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 2, definition_name: None
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -86,7 +86,7 @@
# GitHub App Manifest URLs
GITHUB_MANIFEST_URL = "https://github.com/settings/apps/new"
GITHUB_MANIFEST_CONVERSION_URL = "https://api.github.com/app-manifests/{code}/conversions"
-GITHUB_MANIFEST_TIMEOUT_SECONDS = 10.0
+GITHUB_MANIFEST_TIMEOUT_SECONDS = 11.0
# Required permissions for stampbot
MANIFEST_PERMISSIONS = {........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.06s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 3, definition_name: None
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -86,7 +86,7 @@
# GitHub App Manifest URLs
GITHUB_MANIFEST_URL = "https://github.com/settings/apps/new"
GITHUB_MANIFEST_CONVERSION_URL = "https://api.github.com/app-manifests/{code}/conversions"
-GITHUB_MANIFEST_TIMEOUT_SECONDS = 10.0
+GITHUB_MANIFEST_TIMEOUT_SECONDS = 9.0
# Required permissions for stampbot
MANIFEST_PERMISSIONS = {........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.18s
SURVIVED
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.SURVIVED
operator: core/NumberReplacer, occurrence: 4, definition_name: create_manifest
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -130,7 +130,7 @@
_validate_url(redirect_url, "redirect_url")
# Extract base URL from redirect URL
- base_url = redirect_url.rsplit("/setup/callback", 1)[0]
+ base_url = redirect_url.rsplit("/setup/callback", 2)[0]
manifest: dict[str, Any] = {
"name": app_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
........................................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
461 passed, 3 deselected, 1 warning in 5.02s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 5, definition_name: create_manifest
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -130,7 +130,7 @@
_validate_url(redirect_url, "redirect_url")
# Extract base URL from redirect URL
- base_url = redirect_url.rsplit("/setup/callback", 1)[0]
+ base_url = redirect_url.rsplit("/setup/callback", 0)[0]
manifest: dict[str, Any] = {
"name": app_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.........................................F
=================================== FAILURES ===================================
__________________ TestCreateManifest.test_manifest_base_url ___________________
self = <tests.test_manifest.TestCreateManifest object at 0x7f6bffdf7680>
def test_manifest_base_url(self):
"""Test manifest contains correct base URL derived from redirect URL."""
manifest = create_manifest(
redirect_url="https://stampbot.example.com/setup/callback",
)
> assert manifest["url"] == "https://stampbot.example.com"
E AssertionError: assert 'https://stam...etup/callback' == 'https://stampbot.example.com'
E
E - https://stampbot.example.com
E + https://stampbot.example.com/setup/callback
E ? +++++++++++++++
tests/test_manifest.py:154: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestCreateManifest::test_manifest_base_url - AssertionError: assert 'https://stam...etup/callback' == 'https://stampbot.example.com'
- https://stampbot.example.com
+ https://stampbot.example.com/setup/callback
? +++++++++++++++
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 257 passed, 3 deselected, 1 warning in 3.68s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 6, definition_name: create_manifest
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -130,7 +130,7 @@
_validate_url(redirect_url, "redirect_url")
# Extract base URL from redirect URL
- base_url = redirect_url.rsplit("/setup/callback", 1)[0]
+ base_url = redirect_url.rsplit("/setup/callback", 1)[ 1]
manifest: dict[str, Any] = {
"name": app_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.........................................F
=================================== FAILURES ===================================
__________________ TestCreateManifest.test_manifest_base_url ___________________
self = <tests.test_manifest.TestCreateManifest object at 0x7f92494f7680>
def test_manifest_base_url(self):
"""Test manifest contains correct base URL derived from redirect URL."""
manifest = create_manifest(
redirect_url="https://stampbot.example.com/setup/callback",
)
> assert manifest["url"] == "https://stampbot.example.com"
E AssertionError: assert '' == 'https://stampbot.example.com'
E
E - https://stampbot.example.com
tests/test_manifest.py:154: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestCreateManifest::test_manifest_base_url - AssertionError: assert '' == 'https://stampbot.example.com'
- https://stampbot.example.com
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 257 passed, 3 deselected, 1 warning in 3.61s
worker outcome: WorkerOutcome.NORMAL
test outcome: TestOutcome.KILLED
operator: core/NumberReplacer, occurrence: 7, definition_name: create_manifest
--- mutation diff ---
--- a/stampbot/manifest.py
+++ b/stampbot/manifest.py
@@ -130,7 +130,7 @@
_validate_url(redirect_url, "redirect_url")
# Extract base URL from redirect URL
- base_url = redirect_url.rsplit("/setup/callback", 1)[0]
+ base_url = redirect_url.rsplit("/setup/callback", 1)[ -1]
manifest: dict[str, Any] = {
"name": app_name,........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.........................................F
=================================== FAILURES ===================================
__________________ TestCreateManifest.test_manifest_base_url ___________________
self = <tests.test_manifest.TestCreateManifest object at 0x7fc1a21f7680>
def test_manifest_base_url(self):
"""Test manifest contains correct base URL derived from redirect URL."""
manifest = create_manifest(
redirect_url="https://stampbot.example.com/setup/callback",
)
> assert manifest["url"] == "https://stampbot.example.com"
E AssertionError: assert '' == 'https://stampbot.example.com'
E
E - https://stampbot.example.com
tests/test_manifest.py:154: AssertionError
=============================== warnings summary ===============================
../../../.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1
/home/runner/.cache/pypoetry/virtualenvs/stampbot-q2WA0ezo-py3.14/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_manifest.py::TestCreateManifest::test_manifest_base_url - AssertionError: assert '' == 'https://stampbot.example.com'
- https://stampbot.example.com
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 257 passed, 3 deselected, 1 warning in 3.60s