"""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