feat(mcp-docugen): Task 4-6 template_store, llm_client, generation_store
- TemplateStore: CRUD filesystem + asset dir, frontmatter YAML roundtrip, path traversal rejection - OpenRouterClient: async httpx con retry backoff esponenziale (5xx, 429, timeout), no-retry su 4xx, parse usage/cost - GenerationStore: SQLite aiosqlite con schema generations + ephemeral_assets, cleanup TTL, stats aggregate Root pyproject aggiornato con respx + pytest-cov dev deps. 19 + 11 + 9 + 6 = 45 test totali, tutti passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_docugen.models import TemplateFrontmatter
|
||||
from mcp_docugen.template_store import (
|
||||
InvalidFrontmatter,
|
||||
TemplateAlreadyExists,
|
||||
TemplateNotFound,
|
||||
TemplateStore,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(tmp_path):
|
||||
return TemplateStore(base_dir=tmp_path)
|
||||
|
||||
|
||||
def _fm(name="fattura"):
|
||||
return TemplateFrontmatter(
|
||||
name=name,
|
||||
description="Fattura commerciale",
|
||||
required_variables=[{"name": "cliente", "type": "string"}],
|
||||
)
|
||||
|
||||
|
||||
async def test_create_and_get(store):
|
||||
fm = _fm()
|
||||
await store.create(name="fattura", frontmatter=fm, body="# Body {{cliente}}")
|
||||
got = await store.get("fattura")
|
||||
assert got.frontmatter.name == "fattura"
|
||||
assert "# Body {{cliente}}" in got.body
|
||||
|
||||
|
||||
async def test_create_duplicate_raises(store):
|
||||
fm = _fm()
|
||||
await store.create(name="fattura", frontmatter=fm, body="body")
|
||||
with pytest.raises(TemplateAlreadyExists):
|
||||
await store.create(name="fattura", frontmatter=fm, body="body")
|
||||
|
||||
|
||||
async def test_get_missing_raises(store):
|
||||
with pytest.raises(TemplateNotFound):
|
||||
await store.get("nope")
|
||||
|
||||
|
||||
async def test_list_returns_summaries(store):
|
||||
await store.create(name="a", frontmatter=_fm("a"), body="b")
|
||||
await store.create(name="b", frontmatter=_fm("b"), body="b")
|
||||
result = await store.list()
|
||||
names = sorted(t.name for t in result)
|
||||
assert names == ["a", "b"]
|
||||
|
||||
|
||||
async def test_update_overwrites(store):
|
||||
await store.create(name="f", frontmatter=_fm("f"), body="old")
|
||||
new_fm = TemplateFrontmatter(name="f", description="new description")
|
||||
await store.update(name="f", frontmatter=new_fm, body="new body")
|
||||
got = await store.get("f")
|
||||
assert got.body == "new body"
|
||||
assert got.frontmatter.description == "new description"
|
||||
|
||||
|
||||
async def test_update_missing_raises(store):
|
||||
with pytest.raises(TemplateNotFound):
|
||||
await store.update(name="nope", frontmatter=_fm("nope"), body="x")
|
||||
|
||||
|
||||
async def test_delete_removes(store):
|
||||
await store.create(name="f", frontmatter=_fm("f"), body="b")
|
||||
await store.delete("f")
|
||||
with pytest.raises(TemplateNotFound):
|
||||
await store.get("f")
|
||||
|
||||
|
||||
async def test_delete_missing_raises(store):
|
||||
with pytest.raises(TemplateNotFound):
|
||||
await store.delete("nope")
|
||||
|
||||
|
||||
async def test_assets_are_saved_and_listed(store):
|
||||
png_bytes = b"\x89PNG\r\n\x1a\n"
|
||||
assets = [
|
||||
{
|
||||
"filename": "logo.png",
|
||||
"data_b64": base64.b64encode(png_bytes).decode(),
|
||||
"mime": "image/png",
|
||||
}
|
||||
]
|
||||
await store.create(name="f", frontmatter=_fm("f"), body="b", assets=assets)
|
||||
got = await store.get("f")
|
||||
asset_names = [a.filename for a in got.assets]
|
||||
assert "logo.png" in asset_names
|
||||
content = await store.read_asset("f", "logo.png")
|
||||
assert content == png_bytes
|
||||
|
||||
|
||||
async def test_asset_filename_rejects_path_traversal(store):
|
||||
assets = [{"filename": "../evil.png", "data_b64": "aGk=", "mime": "image/png"}]
|
||||
with pytest.raises(ValueError):
|
||||
await store.create(name="f", frontmatter=_fm("f"), body="b", assets=assets)
|
||||
|
||||
|
||||
async def test_frontmatter_parsing_rejects_malformed_yaml(store, tmp_path):
|
||||
template_dir = tmp_path / "broken"
|
||||
template_dir.mkdir()
|
||||
(template_dir / "template.md").write_text("---\nname: :::broken\n---\nbody")
|
||||
with pytest.raises(InvalidFrontmatter):
|
||||
await store.get("broken")
|
||||
Reference in New Issue
Block a user