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:
2026-10-20 14:58:17 +05:30
parent 56aa1322fb
commit 0cb35d10a6
107 changed files with 3292 additions and 937 deletions

View File

@@ -0,0 +1,79 @@
"""Unit tests for the appointment-reminder pipeline family."""
from pathlib import Path
from pipelines.appointment_reminder.impl import (
AppointmentReminderPipeline,
ReminderComposer,
get_appointment_reminder_pipeline,
)
from pipelines.shared.loader import build_pipeline, load_patterns
RESOURCES = Path(__file__).resolve().parents[2] / "pipelines" / "appointment_reminder" / "resources"
class FakeDoc:
def __init__(self, data):
self._data = data
def model_dump(self, **kwargs):
return self._data
class FakeAppointmentSvc:
def __init__(self, data):
self._data = data
async def read(self, id):
return FakeDoc(self._data) if self._data else None
def _appointment():
return {
"id": 1,
"date": "2026-11-02T10:00:00",
"pet": {"name": "Rex", "species": "dog", "parents": [{"name": "Kim", "email": "kim@example.com"}]},
"vet": {"name": "Dr. Patel"},
}
def test_load_patterns_merges_fragments():
registry = load_patterns(RESOURCES)
assert registry["reminder"]["subject"] == "Appointment reminder"
assert "{pet_name}" in registry["reminder"]["body"]
def test_composer_uses_pattern_and_context():
composer = ReminderComposer()
composer.registry = load_patterns(RESOURCES)
reminder = composer.compose(_appointment(), _appointment()["pet"], _appointment()["pet"]["parents"][0])
assert reminder["to"] == "kim@example.com"
assert reminder["subject"] == "Appointment reminder"
assert "Rex" in reminder["body"] and "Kim" in reminder["body"]
assert "2026-11-02" in reminder["body"]
async def test_full_pipeline_delivers_reminder():
pipeline = get_appointment_reminder_pipeline(FakeAppointmentSvc(_appointment()))
assert isinstance(pipeline, AppointmentReminderPipeline)
result = await pipeline.run(1)
assert result["delivered"] == "log"
assert result["to"] == "kim@example.com"
async def test_pipeline_errors_on_missing_appointment():
pipeline = get_appointment_reminder_pipeline(FakeAppointmentSvc(None))
try:
await pipeline.run(99)
except ValueError as exc:
assert "not found" in str(exc)
else:
raise AssertionError("expected ValueError for missing appointment")
def test_build_pipeline_injects_registry():
pipeline = build_pipeline(
AppointmentReminderPipeline,
resources_dir=RESOURCES,
instances={"appointment_svc": FakeAppointmentSvc(_appointment())},
)
assert pipeline.compose.registry["reminder"]["subject"] == "Appointment reminder"

View File

@@ -1,22 +0,0 @@
from domain.item.models import Item
from domain.job.models import Job
def test_item_defaults():
item = Item(name="widget")
assert item.quantity == 1
def test_job_defaults():
job = Job(name="sync")
assert job.status == "pending"
assert job.retry_count == 0
assert job.error_message is None
def test_job_rejects_unknown_status():
try:
Job(name="sync", status="exploded")
except Exception:
return
raise AssertionError("status should be rejected")

View File

@@ -1,28 +0,0 @@
from types import SimpleNamespace
from pipelines.item_sync import run_item_sync
class FakeItemSvc:
def __init__(self):
self.created = []
async def create(self, data: dict):
self.created.append(dict(data))
async def test_run_item_sync_creates_item():
svc = FakeItemSvc()
job = SimpleNamespace(id="j1", name="widget", payload={"quantity": 4})
await run_item_sync(job, {"item_svc": svc})
assert len(svc.created) == 1
assert svc.created[0] == {"name": "widget", "quantity": 4}
async def test_run_item_sync_uses_payload_name():
svc = FakeItemSvc()
job = SimpleNamespace(id="j1", name="ignored", payload={"name": "gadget"})
await run_item_sync(job, {"item_svc": svc})
assert svc.created[0]["name"] == "gadget"

View File

@@ -0,0 +1,100 @@
"""Unit tests for the jobs interface helpers and routes (memory backend)."""
from types import SimpleNamespace
from interfaces.jobs.routes import (
create_job,
delete_job,
get_job,
list_jobs,
update_job,
)
class FakeDoc:
"""Minimal document exposing the pydantic dump interface."""
def __init__(self, **fields):
self.__dict__.update(fields)
def model_dump(self, **kwargs):
return dict(self.__dict__)
class FakeJobSvc:
"""In-memory job service double."""
def __init__(self):
self.store: dict[int, FakeDoc] = {}
self._next = 1
async def search(self, filter=None):
return list(self.store.values())
async def create(self, body):
body.setdefault("status", "pending")
body.setdefault("retry_count", 0)
doc = FakeDoc(id=self._next, **body)
self.store[self._next] = doc
self._next += 1
return doc
async def read(self, id):
return self.store.get(id)
async def update(self, id, body):
doc = FakeDoc(id=id, **body)
self.store[id] = doc
return doc
async def delete(self, id):
return self.store.pop(id, None) is not None
def _request(svc_obj):
state = SimpleNamespace(db_resources={"job_svc": svc_obj})
return SimpleNamespace(app=SimpleNamespace(state=state))
async def test_create_list_get_update_delete_job():
svc = FakeJobSvc()
request = _request(svc)
response = SimpleNamespace(status_code=200)
created = await create_job(
request, {"name": "remind", "kind": "appointment_reminder"}, response
)
assert response.status_code == 201
assert created["kind"] == "appointment_reminder"
assert created["status"] == "pending"
page = await list_jobs(request)
assert page["total"] == 1
assert page["items"][0]["name"] == "remind"
one = await get_job(request, created["id"])
assert one["id"] == created["id"]
updated = await update_job(
request, created["id"], {"name": "remind-2", "kind": "appointment_reminder"}
)
assert updated["name"] == "remind-2"
response = SimpleNamespace(status_code=200)
await delete_job(request, created["id"], response)
assert response.status_code == 204
assert (await list_jobs(request))["total"] == 0
async def test_list_jobs_filters_by_status():
svc = FakeJobSvc()
request = _request(svc)
await create_job(request, {"name": "a", "kind": "k"}, SimpleNamespace())
await create_job(request, {"name": "b", "kind": "k", "status": "done"}, SimpleNamespace())
pending = await list_jobs(request, status="pending")
assert pending["total"] == 1
assert pending["items"][0]["name"] == "a"
done = await list_jobs(request, status="done")
assert done["total"] == 1
assert done["items"][0]["name"] == "b"

View 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 == []