Phase 7: serve the vet sample from the template tools and docs
- 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
This commit is contained in:
121
standards/mcp/templates/app/tests/unit/test_worker.py
Normal file
121
standards/mcp/templates/app/tests/unit/test_worker.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""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 == []
|
||||
Reference in New Issue
Block a user