Initial version
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""templates.py — template search/ranking and creation flow (plan §9, §13)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from fakes import FakeElabftwClient, FakeTemplate, make_template
|
||||
from mcp_server.errors import NotFoundError
|
||||
|
||||
def ranking_client() -> FakeElabftwClient:
|
||||
return FakeElabftwClient(
|
||||
templates=[
|
||||
FakeTemplate(id=1, title="Cell culture", description="Maintain adherent lines."),
|
||||
FakeTemplate(
|
||||
id=2,
|
||||
title="Protein extraction",
|
||||
description="Lyse cells; collect supernatant in pcr tubes.",
|
||||
),
|
||||
FakeTemplate(id=3, title="Ethanol precipitation", description="Precipitate DNA.", tags=["pcr"]),
|
||||
FakeTemplate(id=4, title="PCR cleanup", description="Purify an amplicon."),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# --- short description ------------------------------------------------------
|
||||
|
||||
|
||||
async def test_short_description_is_first_paragraph(templates_service, client):
|
||||
client.templates[7] = make_template()
|
||||
result = await templates_service.list_templates()
|
||||
assert result[0].short_description == "Purify an amplicon after PCR."
|
||||
|
||||
|
||||
async def test_short_description_truncated_to_limit(templates_service, settings, client):
|
||||
settings.labvoice_template_desc_limit = 20
|
||||
client.templates[7] = make_template(
|
||||
description="P" * 100 + "\n\nSecond paragraph that must not appear."
|
||||
)
|
||||
result = await templates_service.list_templates()
|
||||
summary = result[0]
|
||||
assert len(summary.short_description) <= 20
|
||||
assert "Second paragraph" not in summary.short_description
|
||||
|
||||
|
||||
async def test_short_description_empty_when_no_description(templates_service, client):
|
||||
client.templates[7] = make_template(description="")
|
||||
result = await templates_service.list_templates()
|
||||
assert result[0].short_description == ""
|
||||
|
||||
|
||||
async def test_summary_carries_id_title_tags(templates_service, client):
|
||||
client.templates[7] = make_template()
|
||||
result = await templates_service.list_templates()
|
||||
summary = result[0]
|
||||
assert summary.id == 7
|
||||
assert summary.title == "PCR cleanup"
|
||||
assert summary.tags == ["dna", "cleanup"]
|
||||
|
||||
|
||||
# --- ranking ----------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_no_query_preserves_listing_order(templates_service, client):
|
||||
client.templates.update({t.id: t for t in ranking_client().templates.values()})
|
||||
result = await templates_service.list_templates()
|
||||
assert [t.id for t in result] == [1, 2, 3, 4]
|
||||
|
||||
|
||||
async def test_title_match_ranks_above_tag_and_description_match(templates_service, client):
|
||||
client.templates.update({t.id: t for t in ranking_client().templates.values()})
|
||||
result = await templates_service.list_templates(query="pcr")
|
||||
ids = [t.id for t in result]
|
||||
assert ids.index(4) < ids.index(3) < ids.index(2), (
|
||||
"expected title(4) > tags(3) > description(2) per plan §9 weights"
|
||||
)
|
||||
|
||||
|
||||
async def test_non_matching_templates_rank_below_matches(templates_service, client):
|
||||
client.templates.update({t.id: t for t in ranking_client().templates.values()})
|
||||
result = await templates_service.list_templates(query="pcr")
|
||||
assert result[-1].id == 1
|
||||
|
||||
|
||||
async def test_limit_is_respected(templates_service, client):
|
||||
client.templates.update({t.id: t for t in ranking_client().templates.values()})
|
||||
result = await templates_service.list_templates(query="pcr", limit=2)
|
||||
assert len(result) == 2
|
||||
assert result[0].id == 4
|
||||
|
||||
|
||||
# --- creation ----------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_create_from_template_returns_experiment_and_first_step(templates_service, client):
|
||||
client.templates[7] = make_template()
|
||||
result = await templates_service.create_experiment_from_template(7)
|
||||
assert result.ok is True
|
||||
assert result.experiment_id == 1000
|
||||
assert result.title == "PCR cleanup"
|
||||
assert result.first_step is not None
|
||||
assert result.first_step.id == 41
|
||||
assert result.first_step.finished is False
|
||||
|
||||
|
||||
async def test_create_with_explicit_title(templates_service, client):
|
||||
client.templates[7] = make_template()
|
||||
result = await templates_service.create_experiment_from_template(7, title="My amplicon run")
|
||||
assert result.title == "My amplicon run"
|
||||
|
||||
|
||||
async def test_created_experiment_copies_template_steps_and_annotations(
|
||||
templates_service, client
|
||||
):
|
||||
"""eLabFTW copies template steps — annotations must come along (plan §9)."""
|
||||
from fakes import ANNOTATION_ETHANOL_FIFO, FakeStep
|
||||
|
||||
client.templates[7] = make_template(
|
||||
steps=[FakeStep(id=41, body=f"Add ethanol\n{ANNOTATION_ETHANOL_FIFO}", ordering=1)]
|
||||
)
|
||||
result = await templates_service.create_experiment_from_template(7)
|
||||
created = client.experiments[result.experiment_id]
|
||||
assert "labvoice:v1" in created.steps[0].body
|
||||
|
||||
|
||||
async def test_create_from_unknown_template_fails_cleanly(templates_service, client):
|
||||
with pytest.raises((NotFoundError, KeyError)):
|
||||
await templates_service.create_experiment_from_template(9999)
|
||||
|
||||
|
||||
async def test_search_index_build_is_optional(templates_service, client):
|
||||
"""Lexical fallback works without an explicitly built index (plan §9 fallback)."""
|
||||
client.templates.update({t.id: t for t in ranking_client().templates.values()})
|
||||
result = await templates_service.list_templates(query="pcr")
|
||||
assert result[0].id == 4
|
||||
Reference in New Issue
Block a user