| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- import os
- import pytest
- import requests
- from typing import Any, Dict
- from garden_layer import GardenLayer
- from garden_layer.config import CLONE_OF, GRAPH, KEROSENE_ROOT, MCP_URL
- CYCLE_2026_3 = "http://world.eu.org/example1#ProductionCycle_21d96b6c-179"
- FIXTURE_FILES = ["ganja_test_fixture.ttl"]
- def pytest_report_header(config):
- return "Garden layer tests: requires virtuoso_mcp running at GardenLayer.MCP_URL"
- @pytest.fixture
- def garden_layer():
- layer = GardenLayer()
- try:
- layer.traverse(KEROSENE_ROOT, CLONE_OF, direction="incoming", limit=1)
- except requests.RequestException as exc:
- pytest.skip(f"Cannot reach MCP server: {exc}")
- if os.getenv("MCP_ALLOW_EXAMPLE_LOAD", "false").lower() == "true":
- try:
- layer.load_examples(files=FIXTURE_FILES, graph=GRAPH)
- except Exception as exc:
- pytest.skip(f"Cannot load garden fixtures: {exc}")
- result = layer.traverse(KEROSENE_ROOT, CLONE_OF, direction="incoming", limit=1)
- bindings = result.get("results", {}).get("bindings", [])
- if not bindings:
- pytest.skip("No garden fixture data available; set MCP_ALLOW_EXAMPLE_LOAD=true")
- return layer
- def test_traverse_clone_tree(garden_layer):
- """Walk `cloneOf` incoming edges and ensure the root has clones to inspect."""
- result = garden_layer.traverse(KEROSENE_ROOT, CLONE_OF, direction="incoming", limit=5)
- assert isinstance(result, dict)
- bindings = result.get("results", {}).get("bindings", [])
- assert isinstance(bindings, list)
- assert bindings, "Expected at least one clone binding"
- print(f"Traverse of {KEROSENE_ROOT} returned {len(bindings)} clone candidates.")
- def test_describe_subject(garden_layer):
- """Describe the root plant and surface the top predicates the garden helpers can reuse."""
- summary = garden_layer.describe_subject(KEROSENE_ROOT, limit=5)
- assert isinstance(summary, dict)
- bindings = summary.get("results", {}).get("bindings", [])
- assert bindings, "Expected describe_subject to return bindings"
- triples = [f"{b['predicate']['value']} -> {b['object'].get('value', '<literal>')}" for b in bindings]
- print("describe_subject returned:")
- for triple in triples:
- print(" ", triple)
- def test_path_traverse_lineage(garden_layer):
- """Use the path helper to follow `cloneOf` and observe the lineage step."""
- path = garden_layer.path_traverse(KEROSENE_ROOT, [CLONE_OF], direction="incoming", limit=5)
- assert isinstance(path, dict)
- bindings = path.get("result", {}).get("results", {}).get("bindings", [])
- assert isinstance(bindings, list)
- assert bindings, "Path traverse should find at least one step"
- print(f"Property path {CLONE_OF} produced {len(bindings)} step bindings.")
- def test_property_usage_statistics(garden_layer):
- """Summarize how the `cloneOf` property is used so future helpers can rely on frequency data."""
- stats = garden_layer.property_usage_statistics(CLONE_OF, examples_limit=3)
- assert isinstance(stats, dict)
- count_bindings = stats.get("count", {}).get("results", {}).get("bindings", [])
- assert count_bindings, "Expected usage count bindings"
- usage_count = count_bindings[0].get("usageCount", {}).get("value")
- example_bindings = stats.get("examples", {}).get("results", {}).get("bindings", [])
- assert isinstance(example_bindings, list)
- print(f"cloneOf usage count: {usage_count}")
- print("example bindings:")
- for binding in example_bindings:
- subject = binding.get("subjectLabel", {}).get("value") or binding.get("subject", {}).get("value")
- object_ = binding.get("objectLabel", {}).get("value") or binding.get("object", {}).get("value")
- print(f" - {subject} -> {object_}")
- def test_batch_insert(garden_layer):
- """Batch-insert a TTL snippet and verify the query shape"""
- ttl = '<http://world.eu.org/example1#batch_test_subject> <http://www.w3.org/2000/01/rdf-schema#label> "garden batch" .'
- result = garden_layer.batch_insert(ttl=ttl, graph=GRAPH)
- assert isinstance(result, dict)
- assert "query" in result
- assert "INSERT DATA" in result.get("query", "").upper()
- print("batch_insert generated:")
- print(result.get("query", ""))
- def call_mcp_tool(tool_name: str, payload: Dict[str, Any]) -> Any:
- # Some ontology/traversal calls can be a bit slow depending on Virtuoso load.
- response = requests.post(MCP_URL, json={"tool": tool_name, "input": payload}, timeout=20)
- if response.status_code >= 400:
- raise AssertionError(
- f"{tool_name} failed with {response.status_code}: {response.text}"
- )
- response.raise_for_status()
- body = response.json()
- assert body.get("status") == "ok", f"{tool_name} failed: {body.get('detail') or 'unknown reason'}"
- return body["result"]
- def test_mcp_garden_cycle_plants():
- result = call_mcp_tool(
- "garden_cycle_plants",
- {"cycle_uri": CYCLE_2026_3, "limit": 5},
- )
- bindings = result.get("results", {}).get("bindings", [])
- assert bindings, "Expected at least one plant binding from the cycle"
- print("garden_cycle_plants returned", len(bindings), "plants for cycle", CYCLE_2026_3)
- for binding in bindings:
- plant_uri = binding.get("plant", {}).get("value")
- assert plant_uri, "Each binding should include a plant URI"
- def test_mcp_garden_cycle_plants_latest_when_cycle_uri_missing():
- """MCP compliance: cycle_uri should be optional and default to latest cycle."""
- result = call_mcp_tool(
- "garden_cycle_plants",
- {"limit": 3},
- )
- bindings = result.get("results", {}).get("bindings", [])
- assert bindings, "Expected plant bindings from the latest cycle when cycle_uri is omitted"
- def test_mcp_garden_cycle_list_detailed_has_strain_info():
- """Ensure the detailed cycle list includes strain labels/descriptions and clone siblings."""
- result = call_mcp_tool(
- "garden_cycle_list_detailed",
- {"limit": 10},
- )
- bindings = result.get("results", {}).get("bindings", [])
- assert bindings, "Expected at least one binding from garden_cycle_list_detailed"
- # Check that at least one binding has strainDesc or cloneSiblings populated.
- any_strain_desc = any('strainDesc' in b and b.get('strainDesc', {}).get('value') for b in bindings)
- any_siblings = any('cloneSiblings' in b and b.get('cloneSiblings', {}).get('value') for b in bindings)
- assert any_strain_desc or any_siblings, "Expected strainDesc and/or cloneSiblings to be present"
- def test_tool_surface_trimmed_and_domain_focused():
- """Ensure garden layer exposes only domain-unique prefixed tools."""
- health_url = MCP_URL.rsplit("/", 1)[0] + "/health"
- response = requests.get(health_url, timeout=10)
- response.raise_for_status()
- body = response.json()
- tools = set(body.get("tools", []))
- expected_present = {
- "garden_add_seedling",
- "garden_cycle_plants",
- "garden_latest_cycle_by_dates",
- "garden_clone_to",
- "garden_cycle_list_detailed",
- "garden_reassign_cycle",
- }
- expected_absent = {
- "garden_describe_subject",
- "garden_path_traverse",
- "garden_property_usage_statistics",
- "garden_batch_insert",
- "garden_load_examples",
- }
- missing = expected_present - tools
- assert not missing, f"Missing expected garden tools: {sorted(missing)}"
- leaked = expected_absent & tools
- assert not leaked, f"Found deprecated/redundant garden tools: {sorted(leaked)}"
- def test_load_examples_fixture(garden_layer):
- if os.getenv("MCP_ALLOW_EXAMPLE_LOAD", "false").lower() != "true":
- pytest.skip("MCP_ALLOW_EXAMPLE_LOAD must be true to load example fixtures")
- result = garden_layer.load_examples(files=["test_fixture.ttl"], graph=GRAPH)
- loaded = result.get("loaded", [])
- assert any(item.get("file") == "test_fixture.ttl" for item in loaded), "Expected fixture file to be loaded"
|