From bd22516689b4daf72048f4197d440ed95b1ff44a Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Sat, 13 Jun 2026 13:40:14 +0200 Subject: [PATCH 1/8] fix(docker): pin uv image to v0.11.21 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8f59383..33469a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Base FROM python:3.11-slim AS base -COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +COPY --from=ghcr.io/astral-sh/uv:0.11.21 /uv /usr/local/bin/uv ENV TZ=Europe/Berlin ARG DEBIAN_FRONTEND=noninteractive From 9b6caf6120aee14a6d201eb3691a16e6f1e56f0e Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Sat, 13 Jun 2026 13:40:37 +0200 Subject: [PATCH 2/8] fix(models): add extra=forbid to ItemCreate and ItemUpdate --- src/app/models/item.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/models/item.py b/src/app/models/item.py index 24788a6..efeafb9 100644 --- a/src/app/models/item.py +++ b/src/app/models/item.py @@ -4,11 +4,15 @@ class ItemCreate(BaseModel): + model_config = ConfigDict(extra="forbid") + name: str = Field(min_length=1, max_length=100) description: str | None = Field(default=None, max_length=1000) class ItemUpdate(BaseModel): + model_config = ConfigDict(extra="forbid") + name: str | None = Field(default=None, min_length=1, max_length=100) description: str | None = Field(default=None, max_length=1000) From df7bacb9ce66c2074ff2118b13ad8269dff35052 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Sat, 13 Jun 2026 13:40:49 +0200 Subject: [PATCH 3/8] =?UTF-8?q?fix(store):=20rename=20list=E2=86=92list=5F?= =?UTF-8?q?items,=20add=20limit/offset=20params?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/store/item_store.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/store/item_store.py b/src/app/store/item_store.py index e9c9876..e98726b 100644 --- a/src/app/store/item_store.py +++ b/src/app/store/item_store.py @@ -25,9 +25,9 @@ async def get(self, item_id: str) -> Item: async with self._lock: return self._store[item_id] - async def list(self) -> list[Item]: + async def list_items(self, limit: int = 20, offset: int = 0) -> list[Item]: async with self._lock: - return list(self._store.values()) + return list(self._store.values())[offset : offset + limit] async def update(self, item_id: str, data: ItemUpdate) -> Item: async with self._lock: From dc2a78b2763fc513f456b58bc63d5aac02bd7d19 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Sat, 13 Jun 2026 13:41:39 +0200 Subject: [PATCH 4/8] fix(routes): add limit/offset Query params to GET /items --- src/app/routes/items.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/routes/items.py b/src/app/routes/items.py index dcb2ed0..61e8c5d 100644 --- a/src/app/routes/items.py +++ b/src/app/routes/items.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Query, status from app.models.item import Item, ItemCreate, ItemUpdate from app.store.item_store import ItemStore, get_item_store @@ -17,9 +17,11 @@ async def create_item( @router.get("", response_model=list[Item], status_code=status.HTTP_200_OK) async def list_items( + limit: int = Query(default=20, ge=1, le=100), + offset: int = Query(default=0, ge=0), store: ItemStore = Depends(get_item_store), ) -> list[Item]: - return await store.list() + return await store.list_items(limit=limit, offset=offset) @router.get("/{item_id}", response_model=Item, status_code=status.HTTP_200_OK) From a971d77f75c9d64be795a4039a4a746611741475 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Sat, 13 Jun 2026 13:42:44 +0200 Subject: [PATCH 5/8] fix(tests): inject client fixture in test_main; add pagination tests --- tests/test_items.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 25 +++++++---------------- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/tests/test_items.py b/tests/test_items.py index 31a404c..8c5b1fb 100644 --- a/tests/test_items.py +++ b/tests/test_items.py @@ -1,7 +1,15 @@ import pytest +import pytest_asyncio from httpx import AsyncClient +@pytest_asyncio.fixture(autouse=True) +async def store_reset() -> None: + from app.store.item_store import _item_store + + _item_store._store.clear() + + # POST /items @@ -54,6 +62,46 @@ async def test_list_items_no_404(client: AsyncClient) -> None: assert r.status_code != 404 +@pytest.mark.parametrize( + "limit,offset,expected_count,expected_status", + [ + (20, 0, 3, 200), # default pagination, fewer items than limit + (2, 0, 2, 200), # custom limit clips result + (2, 2, 1, 200), # offset skips into list + (20, 99, 0, 200), # offset beyond length → empty list + ], +) +async def test_list_items_pagination( + client: AsyncClient, + limit: int, + offset: int, + expected_count: int, + expected_status: int, +) -> None: + for i in range(3): + await client.post("/items", json={"name": f"item-{i}"}) + r = await client.get("/items", params={"limit": limit, "offset": offset}) + assert r.status_code == expected_status + assert len(r.json()) == expected_count + + +@pytest.mark.parametrize( + "params,expected_status", + [ + ({"limit": 0}, 422), # below ge=1 + ({"limit": 101}, 422), # above le=100 + ({"offset": -1}, 422), # below ge=0 + ], +) +async def test_list_items_pagination_validation( + client: AsyncClient, + params: dict[str, int], + expected_status: int, +) -> None: + r = await client.get("/items", params=params) + assert r.status_code == expected_status + + # GET /items/{item_id} diff --git a/tests/test_main.py b/tests/test_main.py index 434c6fd..79326b6 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,27 +1,16 @@ -from httpx import ASGITransport, AsyncClient +from httpx import AsyncClient -from app.main import app - -async def test_index_returns_200() -> None: - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as c: - r = await c.get("/") +async def test_index_returns_200(client: AsyncClient) -> None: + r = await client.get("/") assert r.status_code == 200 -async def test_index_contains_app_name() -> None: - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as c: - r = await c.get("/") +async def test_index_contains_app_name(client: AsyncClient) -> None: + r = await client.get("/") assert "agentic-coding-template" in r.text -async def test_index_not_found() -> None: - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as c: - r = await c.get("/nonexistent-route") +async def test_index_not_found(client: AsyncClient) -> None: + r = await client.get("/nonexistent-route") assert r.status_code == 404 From 87fcf8ed5bb98e4d4fcd43b2de1ee3e52d664354 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Sat, 13 Jun 2026 13:52:17 +0200 Subject: [PATCH 6/8] fix(evals): make model frontmatter field optional in agent structural tests --- evals/test_structural.py | 6 ++++-- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/evals/test_structural.py b/evals/test_structural.py index 590d603..50cd212 100644 --- a/evals/test_structural.py +++ b/evals/test_structural.py @@ -90,7 +90,7 @@ def test_agent_frontmatter_parses(agent_file: Path) -> None: @pytest.mark.parametrize("agent_file", _agent_files(), ids=lambda p: p.stem) def test_agent_required_fields(agent_file: Path) -> None: fm, _ = _strip_frontmatter(agent_file.read_text()) - for field in ("name", "description", "model", "tools"): + for field in ("name", "description", "tools"): assert field in fm, f"{agent_file.name}: missing required field '{field}'" assert fm["description"], f"{agent_file.name}: 'description' must be non-empty" @@ -98,7 +98,9 @@ def test_agent_required_fields(agent_file: Path) -> None: @pytest.mark.parametrize("agent_file", _agent_files(), ids=lambda p: p.stem) def test_agent_model_is_known(agent_file: Path) -> None: fm, _ = _strip_frontmatter(agent_file.read_text()) - model = fm.get("model", "") + model = fm.get("model") + if model is None: + return assert model in KNOWN_MODELS, ( f"{agent_file.name}: model '{model}' is not a known Claude model ID. " f"Known: {sorted(KNOWN_MODELS)}" diff --git a/pyproject.toml b/pyproject.toml index 725b54a..4fd3d72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ ignore = ["E402"] [tool.ruff.lint.per-file-ignores] "tests/**" = ["S101"] -"evals/**" = ["S101", "S603", "S607"] +"evals/**" = ["S101", "S603", "S607", "E501"] ".claude/hooks/**" = ["S101", "S603", "S607"] From 0b725e220df07ddc4864ba0e2f87b56971367bff Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Sat, 13 Jun 2026 13:52:46 +0200 Subject: [PATCH 7/8] chore(agents): remove redundant model pins from implementer, planner, ai-service-generator --- .claude/agents/ai-service-generator.md | 1 - .claude/agents/implementer.md | 1 - .claude/agents/planner.md | 1 - 3 files changed, 3 deletions(-) diff --git a/.claude/agents/ai-service-generator.md b/.claude/agents/ai-service-generator.md index f3eaffd..60794df 100644 --- a/.claude/agents/ai-service-generator.md +++ b/.claude/agents/ai-service-generator.md @@ -1,7 +1,6 @@ --- name: ai-service-generator description: Specialist implementer for LLM/AI integration features using the Anthropic SDK. Use instead of the general implementer when the plan involves Claude API calls, streaming, or tool use. Writes src/ and tests/ only. -model: claude-sonnet-4-6 tools: - Read - Edit diff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md index b7f554d..c562891 100644 --- a/.claude/agents/implementer.md +++ b/.claude/agents/implementer.md @@ -1,7 +1,6 @@ --- name: implementer description: Turns a plan from docs/plans/ into working, tested code. Writes src/ and tests/ only. -model: claude-sonnet-4-6 tools: - Read - Edit diff --git a/.claude/agents/planner.md b/.claude/agents/planner.md index bb2a744..bdcd9c2 100644 --- a/.claude/agents/planner.md +++ b/.claude/agents/planner.md @@ -1,7 +1,6 @@ --- name: planner description: Read-only agent that produces implementation plans in docs/plans/. Does not edit source or test files. -model: claude-sonnet-4-6 tools: - Read - Glob From 8f06b59d926af4b3147fd4dff3bbcdc2dc85ae57 Mon Sep 17 00:00:00 2001 From: Vanessa-Ts Date: Sat, 13 Jun 2026 14:15:32 +0200 Subject: [PATCH 8/8] add deny rules for main --- .claude/settings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index 3de2d4b..87fcb3f 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -82,6 +82,10 @@ "WebFetch(domain:huggingface.co)" ], "deny": [ + "Bash(git push * main)", + "Bash(git push *:refs/heads/main)", + "Bash(git checkout main)", + "Bash(git switch main)", "Read(./.env)", "Read(./.env.*)", "Read(**/.env)",