- build.py: _TEMPLATE_COMPONENTS rewritten around the vet sample (53 kinds —
per-resource slices, jobs, worked appointment_reminder family, tests);
seed tokens dropped from the manifest ('sample: vet' instead)
- registry.py: get_app_component(kind) no longer renames item/example seeds;
prompts and tool docs updated to 'copy a slice and adapt'
- templates/README.md, 01_build_recipe, 40_ui_admin, wiki/03 updated off the
old seed scheme; repo inventory/config now exclude vet (docs move under
standards; react/vet itself is deleted in a later phase)
- regenerated MCP bundle, vendored templates, and site output
122 lines
3.4 KiB
Python
122 lines
3.4 KiB
Python
"""Unit tests for the worker dispatch table and job status transitions."""
|
|
from worker import HANDLERS, poll_jobs
|
|
|
|
|
|
class FakeDoc:
|
|
def __init__(self, **fields):
|
|
self.__dict__.update(fields)
|
|
|
|
def model_dump(self, **kwargs):
|
|
return dict(self.__dict__)
|
|
|
|
|
|
class FakeJobSvc:
|
|
def __init__(self, jobs):
|
|
self.jobs = {j.id: j for j in jobs}
|
|
self.patches: list[tuple[int, dict]] = []
|
|
|
|
async def search(self, filter=None):
|
|
filter = filter or {}
|
|
return [j for j in self.jobs.values() if all(j.__dict__.get(k) == v for k, v in filter.items())]
|
|
|
|
async def patch(self, id, body):
|
|
self.patches.append((int(id), body))
|
|
job = self.jobs[int(id)]
|
|
for k, v in body.items():
|
|
setattr(job, k, v)
|
|
|
|
|
|
class FakeAppointmentSvc:
|
|
def __init__(self, data):
|
|
self._data = data
|
|
|
|
async def read(self, id):
|
|
return FakeDoc(**self._data) if self._data else None
|
|
|
|
|
|
def _job(id=1, kind="appointment_reminder", payload=None):
|
|
return FakeDoc(
|
|
id=id,
|
|
name="j",
|
|
kind=kind,
|
|
status="pending",
|
|
payload=payload or {"appointment_id": 1},
|
|
retry_count=0,
|
|
)
|
|
|
|
|
|
def test_dispatch_table_has_appointment_reminder():
|
|
assert "appointment_reminder" in HANDLERS
|
|
|
|
|
|
async def test_poll_jobs_runs_pending_and_marks_done(monkeypatch):
|
|
monkeypatch.setattr("worker.settings.worker_poll_seconds", 0)
|
|
appointment = {
|
|
"id": 1,
|
|
"date": "2026-11-02T10:00:00",
|
|
"pet": {"name": "Rex", "parents": [{"name": "Kim", "email": "kim@example.com"}]},
|
|
}
|
|
job_svc = FakeJobSvc([_job()])
|
|
resources = {"job_svc": job_svc, "appointment_svc": FakeAppointmentSvc(appointment)}
|
|
|
|
import asyncio
|
|
|
|
task = asyncio.create_task(poll_jobs(resources))
|
|
for _ in range(50):
|
|
await asyncio.sleep(0.01)
|
|
if job_svc.patches and job_svc.patches[-1][1].get("status") == "done":
|
|
break
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
statuses = [body["status"] for _, body in job_svc.patches]
|
|
assert statuses == ["processing", "done"]
|
|
|
|
|
|
async def test_poll_jobs_marks_unknown_kind_failed(monkeypatch):
|
|
monkeypatch.setattr("worker.settings.worker_poll_seconds", 0)
|
|
job_svc = FakeJobSvc([_job(kind="nope")])
|
|
resources = {"job_svc": job_svc, "appointment_svc": FakeAppointmentSvc(None)}
|
|
|
|
import asyncio
|
|
|
|
task = asyncio.create_task(poll_jobs(resources))
|
|
for _ in range(50):
|
|
await asyncio.sleep(0.01)
|
|
if job_svc.patches and job_svc.patches[-1][1].get("status") == "failed":
|
|
break
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
last = job_svc.patches[-1][1]
|
|
assert last["status"] == "failed"
|
|
assert "no handler" in last["error_message"]
|
|
assert last["retry_count"] == 1
|
|
|
|
|
|
async def test_poll_jobs_skips_retries_exhausted(monkeypatch):
|
|
monkeypatch.setattr("worker.settings.worker_poll_seconds", 0)
|
|
monkeypatch.setattr("worker.settings.worker_max_retries", 1)
|
|
job = _job()
|
|
job.retry_count = 1
|
|
job_svc = FakeJobSvc([job])
|
|
resources = {"job_svc": job_svc, "appointment_svc": FakeAppointmentSvc(None)}
|
|
|
|
import asyncio
|
|
|
|
task = asyncio.create_task(poll_jobs(resources))
|
|
await asyncio.sleep(0.05)
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
assert job_svc.patches == []
|