Initial version
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"""workflows/setup.py — scan_instance, map_resource, validate_protocol_template (plan §5)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from fakes import FakeItem, make_item
|
||||
|
||||
|
||||
# --- scan_instance ----------------------------------------------------------
|
||||
|
||||
|
||||
async def test_scan_counts_containers_and_markers(setup, client):
|
||||
client.items[13] = make_item(id=13, title="Water bottle", body="plain", containers=[])
|
||||
result = await setup.scan_instance()
|
||||
assert result.containers_seen == 2 # containers on item 12
|
||||
assert result.markers_found == 1 # marker on item 12
|
||||
|
||||
|
||||
async def test_scan_proposes_marker_matches(setup, client):
|
||||
result = await setup.scan_instance()
|
||||
proposal = next(p for p in result.proposals if p.resource_key == "ethanol_absolute")
|
||||
assert proposal.item_id == 12
|
||||
assert proposal.reason is not None
|
||||
|
||||
|
||||
async def test_scan_proposes_exact_title_match_as_suggestion(setup, client):
|
||||
"""Exact title matches are setup-time suggestions with scores (plan §5.4)."""
|
||||
client.items[14] = FakeItem(id=14, title="sodium_azide", body="")
|
||||
result = await setup.scan_instance()
|
||||
proposal = next((p for p in result.proposals if p.item_id == 14), None)
|
||||
assert proposal is not None
|
||||
assert proposal.resource_key == "sodium_azide"
|
||||
|
||||
|
||||
# --- map_resource -----------------------------------------------------------
|
||||
|
||||
|
||||
async def test_map_resource_binds_key(setup, client, resolver_stub):
|
||||
entry = await setup.map_resource("ethanol_absolute", 12)
|
||||
assert entry.resource_key == "ethanol_absolute"
|
||||
assert entry.item_id == 12
|
||||
assert resolver_stub.mappings["ethanol_absolute"] == 12
|
||||
|
||||
|
||||
async def test_map_resource_is_idempotent_upsert(setup, resolver_stub):
|
||||
await setup.map_resource("ethanol_absolute", 12)
|
||||
await setup.map_resource("ethanol_absolute", 12)
|
||||
assert resolver_stub.mappings["ethanol_absolute"] == 12
|
||||
entries = await resolver_stub.list_mappings()
|
||||
assert len([e for e in entries if e.resource_key == "ethanol_absolute"]) == 1
|
||||
|
||||
|
||||
async def test_map_resource_rebinding_updates(setup, resolver_stub):
|
||||
await setup.map_resource("ethanol_absolute", 12)
|
||||
await setup.map_resource("ethanol_absolute", 13)
|
||||
assert resolver_stub.mappings["ethanol_absolute"] == 13
|
||||
|
||||
|
||||
# --- validate_protocol_template ---------------------------------------------
|
||||
|
||||
|
||||
def _annotated_template_client():
|
||||
from fakes import FakeElabftwClient, FakeStep, make_experiment, make_item, make_template
|
||||
|
||||
annotated_step = FakeStep(
|
||||
id=41,
|
||||
body=(
|
||||
"Add ethanol\n"
|
||||
"<!-- labvoice:v1\n"
|
||||
'{"consumables": [{"resource_key": "ethanol_absolute", "quantity": 2.0,'
|
||||
' "unit": "mL"}]}\n'
|
||||
"-->"
|
||||
),
|
||||
ordering=1,
|
||||
)
|
||||
broken_step = FakeStep(
|
||||
id=42, body="Broken\n<!-- labvoice:v1\n{invalid json\n-->", ordering=2
|
||||
)
|
||||
templates = [
|
||||
make_template(
|
||||
id=7,
|
||||
steps=[annotated_step, broken_step],
|
||||
)
|
||||
]
|
||||
return FakeElabftwClient(
|
||||
templates=templates, experiments=[make_experiment()], items=[make_item()]
|
||||
)
|
||||
|
||||
|
||||
async def test_validate_flags_unmapped_resource_key(setup, client, resolver_stub):
|
||||
"""Annotation ok, but the mapping store lacks the key → blocking error."""
|
||||
client.templates[7] = _annotated_template_client().templates[7]
|
||||
resolver_stub.mappings.clear()
|
||||
report = await setup.validate_protocol_template("experiments_templates", 7)
|
||||
assert report.ok is False
|
||||
errors = [i for i in report.issues if i.severity == "error"]
|
||||
assert any(i.resource_key == "ethanol_absolute" for i in errors)
|
||||
|
||||
|
||||
async def test_validate_ok_when_annotation_and_mapping_are_fine(setup, client, resolver_stub):
|
||||
client.templates[7] = _annotated_template_client().templates[7]
|
||||
report = await setup.validate_protocol_template("experiments_templates", 7)
|
||||
assert report.ok is False # the broken json step must block
|
||||
# fix the broken step, then expect a clean report
|
||||
client.templates[7].steps[1].body = "Clean step without annotation"
|
||||
report = await setup.validate_protocol_template("experiments_templates", 7)
|
||||
assert report.ok is True
|
||||
assert report.issues == []
|
||||
|
||||
|
||||
async def test_validate_reports_annotation_error_with_step_id(setup, client):
|
||||
client.templates[7] = _annotated_template_client().templates[7]
|
||||
report = await setup.validate_protocol_template("experiments_templates", 7)
|
||||
issues = [i for i in report.issues if i.step_id == 42]
|
||||
assert issues and issues[0].severity == "error"
|
||||
|
||||
|
||||
async def test_validate_entity_type_is_mirrored_in_report(setup, client):
|
||||
client.templates[7] = _annotated_template_client().templates[7]
|
||||
report = await setup.validate_protocol_template("experiments_templates", 7)
|
||||
assert report.entity_type == "experiments_templates"
|
||||
assert report.entity_id == 7
|
||||
|
||||
|
||||
async def test_validate_experiment_steps_too(setup, client, resolver_stub):
|
||||
"""validate works on live experiments, not only templates."""
|
||||
resolver_stub.mappings.clear()
|
||||
report = await setup.validate_protocol_template("experiments", 123)
|
||||
assert report.entity_type == "experiments"
|
||||
assert report.ok is False
|
||||
|
||||
|
||||
async def test_validate_unknown_entity_fails_cleanly(setup):
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
await setup.validate_protocol_template("experiments", 9999)
|
||||
assert not isinstance(excinfo.value, NotImplementedError)
|
||||
Reference in New Issue
Block a user