Build reusable bot framework
This commit is contained in:
290
tests/unit/test_durable_queues.py
Normal file
290
tests/unit/test_durable_queues.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""Focused unit coverage for durable job and outbound-message state changes."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from core import jobs, outbox
|
||||
|
||||
|
||||
NOW = datetime.now(timezone.utc)
|
||||
MESSAGE_ID = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
|
||||
class RecordingCursor:
|
||||
def __init__(self, one=None, many=None):
|
||||
self.one = list(one or [])
|
||||
self.many = list(many or [])
|
||||
self.executed = []
|
||||
|
||||
def execute(self, query, params=None):
|
||||
self.executed.append((" ".join(query.split()), params))
|
||||
|
||||
def fetchone(self):
|
||||
return self.one.pop(0) if self.one else None
|
||||
|
||||
def fetchall(self):
|
||||
return self.many
|
||||
|
||||
|
||||
@pytest.mark.parametrize("queue", [jobs, outbox])
|
||||
def test_shared_timestamp_and_positive_validation(queue):
|
||||
assert queue._timestamp("2026-01-02T03:04:05Z") == datetime(
|
||||
2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc
|
||||
)
|
||||
assert queue._timestamp(datetime(2026, 1, 2, 3, 4, 5)).tzinfo == timezone.utc
|
||||
|
||||
with pytest.raises(ValueError, match="datetime"):
|
||||
queue._timestamp(123)
|
||||
with pytest.raises(ValueError, match="whole number"):
|
||||
queue._positive(None, "limit")
|
||||
with pytest.raises(ValueError, match="at least 1"):
|
||||
queue._positive(0, "limit")
|
||||
with pytest.raises(ValueError, match="at most 2"):
|
||||
queue._positive(3, "limit", 2)
|
||||
|
||||
|
||||
def test_create_get_and_list_jobs_with_parameterized_filters():
|
||||
created = {"id": "job-one", "status": "pending"}
|
||||
cursor = RecordingCursor(one=[created])
|
||||
result = jobs.create_job(
|
||||
" sample.work ",
|
||||
{"value": 1},
|
||||
NOW,
|
||||
user_uuid="user-one",
|
||||
max_attempts="4",
|
||||
idempotency_key="unique-work",
|
||||
job_id="job-one",
|
||||
cursor=cursor,
|
||||
)
|
||||
assert result == created
|
||||
params = cursor.executed[0][1]
|
||||
assert params["job_type"] == "sample.work"
|
||||
assert params["max_attempts"] == 4
|
||||
assert params["payload"].adapted == {"value": 1}
|
||||
|
||||
with pytest.raises(ValueError, match="job_type"):
|
||||
jobs.create_job(" ", {}, NOW, cursor=cursor)
|
||||
|
||||
cursor = RecordingCursor(one=[created])
|
||||
assert jobs.get_job("job-one", cursor=cursor) == created
|
||||
|
||||
cursor = RecordingCursor(many=[created])
|
||||
assert jobs.list_jobs(
|
||||
user_uuid="user-one",
|
||||
status="pending",
|
||||
job_type="sample.work",
|
||||
limit=3,
|
||||
cursor=cursor,
|
||||
) == [created]
|
||||
query, params = cursor.executed[0]
|
||||
assert "user_uuid = %s" in query and "job_type = %s" in query
|
||||
assert params == ["user-one", "pending", "sample.work", 3]
|
||||
|
||||
|
||||
def test_job_claim_renew_complete_retry_and_cancel_paths():
|
||||
claimed = [{"id": "job-one", "status": "running"}]
|
||||
cursor = RecordingCursor(many=claimed)
|
||||
assert jobs.claim_due_jobs(
|
||||
"worker-one",
|
||||
limit=2,
|
||||
lease_seconds=60,
|
||||
job_types="sample.work",
|
||||
cursor=cursor,
|
||||
) == claimed
|
||||
assert len(cursor.executed) == 2
|
||||
assert cursor.executed[1][1]["job_types"] == ["sample.work"]
|
||||
assert jobs.claim_due_jobs("worker", job_types=[], cursor=cursor) == []
|
||||
with pytest.raises(ValueError, match="worker_id"):
|
||||
jobs.claim_due_jobs("", cursor=cursor)
|
||||
|
||||
updated = {"id": "job-one", "status": "running"}
|
||||
cursor = RecordingCursor(one=[updated, {**updated, "status": "completed"}])
|
||||
assert jobs.renew_job_lease("job-one", "worker-one", 30, cursor=cursor) == updated
|
||||
assert jobs.complete_job("job-one", "worker-one", cursor=cursor)[
|
||||
"status"
|
||||
] == "completed"
|
||||
|
||||
cursor = RecordingCursor(
|
||||
one=[
|
||||
{"attempts": 2, "max_attempts": 3},
|
||||
{"id": "job-one", "status": "pending"},
|
||||
]
|
||||
)
|
||||
retried = jobs.fail_job(
|
||||
"job-one", "worker-one", "temporary", retry_seconds=10, cursor=cursor
|
||||
)
|
||||
assert retried["status"] == "pending"
|
||||
assert cursor.executed[1][1]["delay"] == 20
|
||||
assert cursor.executed[1][1]["exhausted"] is False
|
||||
|
||||
cursor = RecordingCursor(
|
||||
one=[
|
||||
{"attempts": 3, "max_attempts": 3},
|
||||
{"id": "job-one", "status": "failed"},
|
||||
]
|
||||
)
|
||||
assert jobs.fail_job("job-one", "worker-one", "fatal", cursor=cursor)[
|
||||
"status"
|
||||
] == "failed"
|
||||
assert cursor.executed[1][1]["exhausted"] is True
|
||||
assert jobs.fail_job(
|
||||
"missing", "worker-one", "ignored", cursor=RecordingCursor()
|
||||
) is None
|
||||
|
||||
cursor = RecordingCursor(one=[{"id": "job-one", "status": "cancelled"}])
|
||||
assert jobs.cancel_job("job-one", user_uuid="user-one", cursor=cursor)[
|
||||
"status"
|
||||
] == "cancelled"
|
||||
cursor = RecordingCursor(many=[{"id": "job-two"}])
|
||||
assert jobs.cancel_jobs(job_type="sample.work", cursor=cursor) == [
|
||||
{"id": "job-two"}
|
||||
]
|
||||
with pytest.raises(ValueError, match="filter"):
|
||||
jobs.cancel_jobs(cursor=cursor)
|
||||
|
||||
|
||||
def test_enqueue_get_and_list_messages_with_parameterized_filters():
|
||||
created = {"id": "message-one", "status": "pending"}
|
||||
cursor = RecordingCursor(one=[created])
|
||||
result = outbox.enqueue_message(
|
||||
"user-one",
|
||||
" discord_dm ",
|
||||
{"content": "hello"},
|
||||
"unique-message",
|
||||
available_at=NOW,
|
||||
max_attempts=4,
|
||||
message_id="message-one",
|
||||
cursor=cursor,
|
||||
)
|
||||
assert result == created
|
||||
params = cursor.executed[0][1]
|
||||
assert params["channel"] == "discord_dm"
|
||||
assert params["payload"].adapted == {"content": "hello"}
|
||||
|
||||
invalidValues = [
|
||||
(None, "discord_dm", {}, "key", "user_uuid"),
|
||||
("user", " ", {}, "key", "channel"),
|
||||
("user", "discord_dm", {}, None, "idempotency_key"),
|
||||
("user", "discord_dm", None, "key", "payload"),
|
||||
]
|
||||
for userUUID, channel, payload, key, error in invalidValues:
|
||||
with pytest.raises(ValueError, match=error):
|
||||
outbox.enqueue_message(
|
||||
userUUID, channel, payload, key, available_at=NOW, cursor=cursor
|
||||
)
|
||||
|
||||
cursor = RecordingCursor(one=[created])
|
||||
assert outbox.get_message(MESSAGE_ID, cursor=cursor) == created
|
||||
assert outbox.get_message("not-a-uuid", cursor=cursor) is None
|
||||
cursor = RecordingCursor(many=[created])
|
||||
assert outbox.list_messages(
|
||||
user_uuid="user-one",
|
||||
status="pending",
|
||||
channel="discord_dm",
|
||||
limit=2,
|
||||
cursor=cursor,
|
||||
) == [created]
|
||||
|
||||
|
||||
def test_outbox_claim_renew_delivery_retry_and_cancel_paths():
|
||||
claimed = [{"id": "message-one", "status": "delivering"}]
|
||||
cursor = RecordingCursor(many=claimed)
|
||||
assert outbox.claim_messages(
|
||||
"worker-one",
|
||||
channel="discord_dm",
|
||||
limit=2,
|
||||
lease_seconds=60,
|
||||
cursor=cursor,
|
||||
) == claimed
|
||||
assert len(cursor.executed) == 2
|
||||
with pytest.raises(ValueError, match="worker_id"):
|
||||
outbox.claim_messages(None, cursor=cursor)
|
||||
|
||||
cursor = RecordingCursor(
|
||||
one=[
|
||||
{"id": "message-one", "status": "delivering"},
|
||||
{"id": "message-one", "status": "delivered"},
|
||||
]
|
||||
)
|
||||
assert outbox.renew_message_lease(
|
||||
"message-one", "worker-one", 60, cursor=cursor
|
||||
)["status"] == "delivering"
|
||||
delivered = outbox.mark_delivered(
|
||||
"message-one",
|
||||
"worker-one",
|
||||
external_message_id="discord-123",
|
||||
cursor=cursor,
|
||||
)
|
||||
assert delivered["status"] == "delivered"
|
||||
assert cursor.executed[1][1][0] == "discord-123"
|
||||
|
||||
cursor = RecordingCursor(
|
||||
one=[
|
||||
{"attempts": 1, "max_attempts": 3},
|
||||
{"id": "message-one", "status": "pending"},
|
||||
]
|
||||
)
|
||||
retried = outbox.retry_message(
|
||||
"message-one", "worker-one", "temporary", retry_seconds=15, cursor=cursor
|
||||
)
|
||||
assert retried["status"] == "pending"
|
||||
assert cursor.executed[1][1]["delay"] == 15
|
||||
|
||||
cursor = RecordingCursor(
|
||||
one=[
|
||||
{"attempts": 3, "max_attempts": 3},
|
||||
{"id": "message-one", "status": "failed"},
|
||||
]
|
||||
)
|
||||
assert outbox.retry_message(
|
||||
"message-one", "worker-one", "fatal", cursor=cursor
|
||||
)["status"] == "failed"
|
||||
assert outbox.retry_message(
|
||||
"missing", "worker-one", "ignored", cursor=RecordingCursor()
|
||||
) is None
|
||||
|
||||
cursor = RecordingCursor(one=[{"id": "message-one", "status": "cancelled"}])
|
||||
assert outbox.cancel_message(
|
||||
"message-one", user_uuid="user-one", cursor=cursor
|
||||
)["status"] == "cancelled"
|
||||
cursor = RecordingCursor(many=[{"id": "message-two"}])
|
||||
assert outbox.cancel_messages(channel="discord_dm", cursor=cursor) == [
|
||||
{"id": "message-two"}
|
||||
]
|
||||
with pytest.raises(ValueError, match="filter"):
|
||||
outbox.cancel_messages(cursor=cursor)
|
||||
|
||||
|
||||
def test_retry_backoff_is_capped():
|
||||
cursor = RecordingCursor(
|
||||
one=[
|
||||
{"attempts": 20, "max_attempts": 30},
|
||||
{"id": "job", "status": "pending"},
|
||||
]
|
||||
)
|
||||
jobs.fail_job(
|
||||
"job",
|
||||
"worker",
|
||||
"retry",
|
||||
retry_seconds=30,
|
||||
max_retry_seconds=90,
|
||||
cursor=cursor,
|
||||
)
|
||||
assert cursor.executed[1][1]["delay"] == 90
|
||||
|
||||
cursor = RecordingCursor(
|
||||
one=[
|
||||
{"attempts": 20, "max_attempts": 30},
|
||||
{"id": "message", "status": "pending"},
|
||||
]
|
||||
)
|
||||
outbox.retry_message(
|
||||
"message",
|
||||
"worker",
|
||||
"retry",
|
||||
retry_seconds=30,
|
||||
max_retry_seconds=90,
|
||||
cursor=cursor,
|
||||
)
|
||||
assert cursor.executed[1][1]["delay"] == 90
|
||||
Reference in New Issue
Block a user