#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" if [[ -f .env ]]; then set -a # shellcheck source=/dev/null source .env set +a fi PORT=8501 BASE_URL="http://127.0.0.1:$PORT" TEST_GRAPH="http://world.eu.org/cannabis-breeding#test" if ! command -v jq >/dev/null 2>&1; then echo "ERROR: jq is required for test output parsing." exit 1 fi pass_count=0 fail_count=0 section() { echo echo "============================================================" echo "$1" echo "============================================================" } pass() { pass_count=$((pass_count + 1)) echo "✅ PASS: $1" } fail() { fail_count=$((fail_count + 1)) echo "❌ FAIL: $1" echo " detail: $2" } call_mcp() { local payload="$1" curl -sS -X POST "$BASE_URL/mcp" \ -H "Content-Type: application/json" \ -d "$payload" } assert_tool_ok() { local label="$1" local payload="$2" local response response="$(call_mcp "$payload")" || { fail "$label" "HTTP request failed" return 1 } if ! echo "$response" | jq -e . >/dev/null 2>&1; then fail "$label" "Non-JSON response: $response" return 1 fi local status status="$(echo "$response" | jq -r '.status // empty')" if [[ "$status" != "ok" ]]; then local detail detail="$(echo "$response" | jq -r '.detail // .error // "unknown error"')" fail "$label" "$detail" return 1 fi pass "$label" TOOL_LAST_RESPONSE="$response" return 0 } section "Health check" root_json="$(curl -sS "$BASE_URL/")" echo "$root_json" | jq '{status, virtuoso, tools, guardrails}' if [[ "$(echo "$root_json" | jq -r '.status // empty')" == "MCP server running" ]]; then pass "root endpoint" else fail "root endpoint" "unexpected status" fi section "Read-only tools" TOOL_LAST_RESPONSE="" if assert_tool_ok "list_graphs" '{"tool":"list_graphs","input":{}}'; then echo "Graphs returned:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].g.value' | sed 's/^/ - /' || true fi section "Update path (single triple + small Turtle)" if [[ "${MCP_ALLOW_EXAMPLE_LOAD:-false}" == "true" ]]; then TOOL_LAST_RESPONSE="" if assert_tool_ok "insert_triple" '{"tool":"insert_triple","input":{"subject":"http://world.eu.org/example1#TestPlant1","predicate":"http://www.w3.org/1999/02/22-rdf-syntax-ns#type","object":"http://world.eu.org/cannabis-breeding#IndividualPlant","object_type":"uri","graph":"http://world.eu.org/cannabis-breeding#test"}}'; then echo "Inserted query preview:" echo "$TOOL_LAST_RESPONSE" | jq -r '.query' | sed 's/^/ /' fi TOOL_LAST_RESPONSE="" if assert_tool_ok "load_examples (productioncycle_export.ttl)" '{"tool":"load_examples","input":{"files":["productioncycle_export.ttl"],"graph":"http://world.eu.org/cannabis-breeding#test"}}'; then echo "Loaded files:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.loaded[] | " - \(.file) -> \(.graph)"' fi TOOL_LAST_RESPONSE="" if assert_tool_ok "load_examples (export large TTL)" '{"tool":"load_examples","input":{"files":["export_2026-03-26T14_10_20.ttl"],"graph":"http://world.eu.org/cannabis-breeding#test"}}'; then echo "Loaded files:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.loaded[] | " - \(.file) -> \(.graph)"' fi else echo "â„šī¸ Skipped update tests (MCP_ALLOW_EXAMPLE_LOAD != true)" fi section "Helper retrieval checks" TOOL_LAST_RESPONSE="" if assert_tool_ok "get_entities_by_type(Strain)" '{"tool":"get_entities_by_type","input":{"type_uri":"http://world.eu.org/cannabis-breeding#Strain","limit":5}}'; then echo "Entity IRIs:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].s.value' | sed 's/^/ - /' || true fi TOOL_LAST_RESPONSE="" if assert_tool_ok "search_label(term=King)" '{"tool":"search_label","input":{"term":"King","limit":5}}'; then echo "Label hits:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.label.value) (\(.s.value))"' || true fi TOOL_LAST_RESPONSE="" if assert_tool_ok "get_predicates_for_subject(Strain_king_kong)" '{"tool":"get_predicates_for_subject","input":{"subject_uri":"http://world.eu.org/example1#Strain_king_kong","limit":10}}'; then echo "Predicates found:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].p.value' | sed 's/^/ - /' || true fi TOOL_LAST_RESPONSE="" if assert_tool_ok "get_labels_for_subject(Strain_king_kong)" '{"tool":"get_labels_for_subject","input":{"subject_uri":"http://world.eu.org/example1#Strain_king_kong"}}'; then echo "Subject labels:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].label.value' | sed 's/^/ - /' || true fi section "Clone inspection" KEROSENE_ROOT="http://world.eu.org/example1#Plant_90d53925-bb5" TOOL_LAST_RESPONSE="" if assert_tool_ok "traverse_property(cloneOf incoming)" '{"tool":"traverse_property","input":{"subject_uri":"http://world.eu.org/example1#Plant_90d53925-bb5","property_uri":"http://world.eu.org/cannabis-breeding#cloneOf","direction":"incoming","limit":20}}'; then echo "Clones of Kerosene Krash root:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.neighbor.value) (label: \(.label.value // ""))"' fi section "Ontology discovery" TOOL_LAST_RESPONSE="" if assert_tool_ok "list_classes(term=cycle)" '{"tool":"list_classes","input":{"term":"cycle","limit":10}}'; then echo "Class hits:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.class.value) (\(.label.value // ""))"' || true fi TOOL_LAST_RESPONSE="" if assert_tool_ok "list_properties(term=clone)" '{"tool":"list_properties","input":{"term":"clone","limit":10}}'; then echo "Property hits:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.property.value) (\(.label.value // ""))"' || true fi TOOL_LAST_RESPONSE="" if assert_tool_ok "describe_property(cb:cloneOf)" '{"tool":"describe_property","input":{"property_uri":"http://world.eu.org/cannabis-breeding#cloneOf","usage_limit":5}}'; then echo "cloneOf metadata rows:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.metadata.results.bindings | length | tostring | " - rows: " + .' fi section "Subject/path helpers" PATH_SUBJECT="http://world.eu.org/example1#Plant_cookie_kerosene_2026_3" TOOL_LAST_RESPONSE="" if assert_tool_ok "describe_subject(cookie path)" "{\"tool\":\"describe_subject\",\"input\":{\"subject_uri\":\"${PATH_SUBJECT}\",\"limit\":10}}"; then echo "Subject triples:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.predicate.value) -> \(.objectLabel.value // .object.value)"' || true fi TOOL_LAST_RESPONSE="" if assert_tool_ok "path_traverse(seed lineage)" "{\"tool\":\"path_traverse\",\"input\":{\"subject_uri\":\"${PATH_SUBJECT}\",\"property_path\":[\"http://world.eu.org/cannabis-breeding#grownFromSeedProduct\",\"http://world.eu.org/cannabis-breeding#seedProductFromPollination\"],\"limit\":5}}"; then echo "Path traverse results:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.result.results.bindings[] | " - " + (.n1Label.value // .n1.value) + " -> " + (.n2Label.value // .n2.value)' || true fi TOOL_LAST_RESPONSE="" if assert_tool_ok "property_usage_statistics" '{"tool":"property_usage_statistics","input":{"property_uri":"http://world.eu.org/cannabis-breeding#cloneOf","examples_limit":3}}'; then echo "cloneOf usage count:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.count.results.bindings[0].usageCount.value // "0" | " - " + .' || true echo "Sample bindings:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.examples.results.bindings[] | " - " + (.subjectLabel.value // .subject.value) + " -> " + (.objectLabel.value // .object.value)' || true fi TOOL_LAST_RESPONSE="" if assert_tool_ok "batch_insert(test)" '{"tool":"batch_insert","input":{"ttl":" \"batch helper\" .","graph":"http://world.eu.org/cannabis-breeding#test"}}'; then echo "Batch insert query:" echo "$TOOL_LAST_RESPONSE" | jq -r '.result.query' | sed 's/^/ /' fi section "Summary" echo "Passed: $pass_count" echo "Failed: $fail_count" if [[ "$fail_count" -gt 0 ]]; then exit 1 fi echo "All checks passed."