291 lines
9.1 KiB
Python
291 lines
9.1 KiB
Python
"""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
|