first
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""Tests for worker handlers + claim/persistence logic.
|
||||
|
||||
All tests run in-memory using FakeJobsCollection — no Mongo/Rabbit required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from asf_domain import JobStatus, JobUsage
|
||||
|
||||
|
||||
class TestEchoHandler:
|
||||
@pytest.mark.asyncio
|
||||
async def test_echo_handler_returns_payload_as_result(self, worker_deps, sample_envelope):
|
||||
from app.handlers import echo_handler
|
||||
res = await echo_handler(sample_envelope, worker_deps)
|
||||
assert res.status == JobStatus.COMPLETED
|
||||
assert res.result["echo"] == sample_envelope.payload
|
||||
assert res.result["handled_by"] == worker_deps.worker_id
|
||||
assert isinstance(res.usage, JobUsage)
|
||||
assert res.usage.cpu_seconds > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_noop_handler_marks_failed_when_queue_unregistered(self, worker_deps, sample_envelope):
|
||||
from app.handlers import noop_handler
|
||||
res = await noop_handler(sample_envelope, worker_deps)
|
||||
assert res.status == JobStatus.FAILED
|
||||
assert "No worker handler registered" in res.error
|
||||
assert res.error_detail is not None and "hint" in res.error_detail
|
||||
|
||||
|
||||
class TestMarkJobDonePersistence:
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_job_done_writes_status_result_usage(self, worker_deps, sample_envelope, fake_jobs_collection):
|
||||
from app.handlers import mark_job_done, HandlerResult
|
||||
fake_jobs_collection._docs[sample_envelope.job_id] = {
|
||||
"_id": sample_envelope.job_id,
|
||||
"status": JobStatus.RUNNING.value,
|
||||
}
|
||||
result = HandlerResult(
|
||||
status=JobStatus.COMPLETED,
|
||||
result={"echo": {"ok": True}},
|
||||
usage=JobUsage(cpu_seconds=0.042, input_tokens=100, output_tokens=200),
|
||||
)
|
||||
await mark_job_done(worker_deps, sample_envelope.job_id, result)
|
||||
doc = fake_jobs_collection._docs[sample_envelope.job_id]
|
||||
assert doc["status"] == JobStatus.COMPLETED.value
|
||||
assert doc["result"] == result.result
|
||||
assert doc["worker"] == worker_deps.worker_id
|
||||
assert doc["usage"]["cpu_seconds"] == 0.042
|
||||
assert doc["usage"]["input_tokens"] == 100
|
||||
assert doc["completed_at"] is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_job_done_records_error_when_failed(self, worker_deps, sample_envelope, fake_jobs_collection):
|
||||
from app.handlers import mark_job_done, HandlerResult
|
||||
fake_jobs_collection._docs[sample_envelope.job_id] = {"_id": sample_envelope.job_id}
|
||||
await mark_job_done(
|
||||
worker_deps,
|
||||
sample_envelope.job_id,
|
||||
HandlerResult(
|
||||
status=JobStatus.FAILED,
|
||||
error="BOOM",
|
||||
error_detail={"stage": "analyze"},
|
||||
),
|
||||
)
|
||||
doc = fake_jobs_collection._docs[sample_envelope.job_id]
|
||||
assert doc["status"] == JobStatus.FAILED.value
|
||||
assert doc["error"] == "BOOM"
|
||||
assert doc["error_detail"] == {"stage": "analyze"}
|
||||
# result / usage keys are ABSENT when empty
|
||||
assert "result" not in doc
|
||||
|
||||
|
||||
class TestClaimRunningAtomic:
|
||||
@pytest.mark.asyncio
|
||||
async def test_claim_sets_running_and_inc_retries(self, worker_deps, sample_envelope, fake_jobs_collection):
|
||||
from app.worker import _claim_running
|
||||
fake_jobs_collection._docs[sample_envelope.job_id] = {
|
||||
"_id": sample_envelope.job_id,
|
||||
"tenant_id": sample_envelope.tenant_id,
|
||||
"status": JobStatus.QUEUED.value,
|
||||
"retries": 0,
|
||||
}
|
||||
await _claim_running(worker_deps, sample_envelope)
|
||||
doc = fake_jobs_collection._docs[sample_envelope.job_id]
|
||||
assert doc["status"] == JobStatus.RUNNING.value
|
||||
assert doc["worker"] == worker_deps.worker_id
|
||||
assert doc["retries"] == 1
|
||||
assert doc["started_at"] is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_claim_bails_if_job_already_terminal(self, worker_deps, sample_envelope, fake_jobs_collection):
|
||||
from app.worker import _claim_running
|
||||
fake_jobs_collection._docs[sample_envelope.job_id] = {
|
||||
"_id": sample_envelope.job_id,
|
||||
"tenant_id": sample_envelope.tenant_id,
|
||||
"status": JobStatus.COMPLETED.value, # not QUEUED
|
||||
"worker": "some-other-worker",
|
||||
}
|
||||
await _claim_running(worker_deps, sample_envelope)
|
||||
doc = fake_jobs_collection._docs[sample_envelope.job_id]
|
||||
# still terminal + still original worker -> no overwrites
|
||||
assert doc["status"] == JobStatus.COMPLETED.value
|
||||
assert doc["worker"] == "some-other-worker"
|
||||
assert "retries" not in doc or doc["retries"] == 0
|
||||
|
||||
|
||||
class TestAIMockedGatewayHandler:
|
||||
"""Ensure ai_handler works safely with no real AI gateway attached (fallback path)."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ai_handler_without_gateway_returns_payload_note(self, worker_deps, sample_envelope):
|
||||
from asf_domain import JobType
|
||||
sample_envelope.type = JobType.AI_INTERACTIVE.value
|
||||
sample_envelope.payload = {"kind": "ask_asf", "question": "Summarize REQ-103"}
|
||||
worker_deps.ai_gateway = None
|
||||
from app.handlers import ai_handler
|
||||
res = await ai_handler(sample_envelope, worker_deps)
|
||||
assert res.status == JobStatus.COMPLETED
|
||||
assert "ai_gateway not attached" in res.result["note"]
|
||||
assert res.result["payload"]["kind"] == "ask_asf"
|
||||
|
||||
|
||||
class TestHandlersQueueContract:
|
||||
def test_expected_queues_have_registered_handlers(self):
|
||||
from app.worker import HANDLERS_BY_QUEUE
|
||||
expected = {
|
||||
"asf.ai.interactive",
|
||||
"asf.ai.background",
|
||||
"asf.indexing",
|
||||
"asf.code_analysis",
|
||||
"asf.testing",
|
||||
"asf.devops",
|
||||
"asf.notifications",
|
||||
}
|
||||
assert set(HANDLERS_BY_QUEUE.keys()) == expected
|
||||
Reference in New Issue
Block a user