test.sh 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. cd "$SCRIPT_DIR"
  5. if [[ -f .env ]]; then
  6. set -a
  7. # shellcheck source=/dev/null
  8. source .env
  9. set +a
  10. fi
  11. PORT=8501
  12. BASE_URL="http://127.0.0.1:$PORT"
  13. TEST_GRAPH="http://world.eu.org/cannabis-breeding#test"
  14. if ! command -v jq >/dev/null 2>&1; then
  15. echo "ERROR: jq is required for test output parsing."
  16. exit 1
  17. fi
  18. pass_count=0
  19. fail_count=0
  20. section() {
  21. echo
  22. echo "============================================================"
  23. echo "$1"
  24. echo "============================================================"
  25. }
  26. pass() {
  27. pass_count=$((pass_count + 1))
  28. echo "✅ PASS: $1"
  29. }
  30. fail() {
  31. fail_count=$((fail_count + 1))
  32. echo "❌ FAIL: $1"
  33. echo " detail: $2"
  34. }
  35. call_mcp() {
  36. local payload="$1"
  37. curl -sS -X POST "$BASE_URL/mcp" \
  38. -H "Content-Type: application/json" \
  39. -d "$payload"
  40. }
  41. assert_tool_ok() {
  42. local label="$1"
  43. local payload="$2"
  44. local response
  45. response="$(call_mcp "$payload")" || {
  46. fail "$label" "HTTP request failed"
  47. return 1
  48. }
  49. if ! echo "$response" | jq -e . >/dev/null 2>&1; then
  50. fail "$label" "Non-JSON response: $response"
  51. return 1
  52. fi
  53. local status
  54. status="$(echo "$response" | jq -r '.status // empty')"
  55. if [[ "$status" != "ok" ]]; then
  56. local detail
  57. detail="$(echo "$response" | jq -r '.detail // .error // "unknown error"')"
  58. fail "$label" "$detail"
  59. return 1
  60. fi
  61. pass "$label"
  62. TOOL_LAST_RESPONSE="$response"
  63. return 0
  64. }
  65. section "Health check"
  66. root_json="$(curl -sS "$BASE_URL/")"
  67. echo "$root_json" | jq '{status, virtuoso, tools, guardrails}'
  68. if [[ "$(echo "$root_json" | jq -r '.status // empty')" == "MCP server running" ]]; then
  69. pass "root endpoint"
  70. else
  71. fail "root endpoint" "unexpected status"
  72. fi
  73. section "Read-only tools"
  74. TOOL_LAST_RESPONSE=""
  75. if assert_tool_ok "list_graphs" '{"tool":"list_graphs","input":{}}'; then
  76. echo "Graphs returned:"
  77. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].g.value' | sed 's/^/ - /' || true
  78. fi
  79. section "Update path (fabricated triples)"
  80. TOOL_LAST_RESPONSE=""
  81. if assert_tool_ok "insert_triple" '{"tool":"insert_triple","input":{"subject":"http://example.org/plain#TestPlant1","predicate":"http://www.w3.org/1999/02/22-rdf-syntax-ns#type","object":"http://example.org/plain#Specimen","object_type":"uri","graph":"http://world.eu.org/cannabis-breeding#test"}}'; then
  82. echo "Inserted query preview:"
  83. echo "$TOOL_LAST_RESPONSE" | jq -r '.query' | sed 's/^/ /'
  84. fi
  85. EXAMPLE_TTL=$(cat <<'EOF'
  86. @prefix ex: <http://example.org/plain#> .
  87. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
  88. ex:PlainPlant a ex:Specimen ;
  89. rdfs:label "Plain test plant" .
  90. EOF
  91. )
  92. payload=$(jq -n --arg ttl "$EXAMPLE_TTL" --arg graph "$TEST_GRAPH" '{"tool":"batch_insert","input":{"ttl":$ttl,"graph":$graph}}')
  93. TOOL_LAST_RESPONSE=""
  94. if assert_tool_ok "batch_insert (fabricated TTL)" "$payload"; then
  95. echo "Batch insert query:"
  96. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.query' | sed 's/^/ /'
  97. fi
  98. section "Helper retrieval checks"
  99. TOOL_LAST_RESPONSE=""
  100. 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
  101. echo "Entity IRIs:"
  102. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].s.value' | sed 's/^/ - /' || true
  103. fi
  104. TOOL_LAST_RESPONSE=""
  105. if assert_tool_ok "search_label(term=King)" '{"tool":"search_label","input":{"term":"King","limit":5}}'; then
  106. echo "Label hits:"
  107. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.label.value) (\(.s.value))"' || true
  108. fi
  109. TOOL_LAST_RESPONSE=""
  110. 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
  111. echo "Predicates found:"
  112. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].p.value' | sed 's/^/ - /' || true
  113. fi
  114. TOOL_LAST_RESPONSE=""
  115. 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
  116. echo "Subject labels:"
  117. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].label.value' | sed 's/^/ - /' || true
  118. fi
  119. section "Clone inspection"
  120. KEROSENE_ROOT="http://world.eu.org/example1#Plant_90d53925-bb5"
  121. TOOL_LAST_RESPONSE=""
  122. 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
  123. echo "Clones of Kerosene Krash root:"
  124. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.neighbor.value) (label: \(.label.value // "<no label>"))"'
  125. fi
  126. section "Ontology discovery"
  127. TOOL_LAST_RESPONSE=""
  128. if assert_tool_ok "list_classes(term=cycle)" '{"tool":"list_classes","input":{"term":"cycle","limit":10}}'; then
  129. echo "Class hits:"
  130. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.class.value) (\(.label.value // "<no label>"))"' || true
  131. fi
  132. TOOL_LAST_RESPONSE=""
  133. if assert_tool_ok "list_properties(term=clone)" '{"tool":"list_properties","input":{"term":"clone","limit":10}}'; then
  134. echo "Property hits:"
  135. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.property.value) (\(.label.value // "<no label>"))"' || true
  136. fi
  137. TOOL_LAST_RESPONSE=""
  138. 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
  139. echo "cloneOf metadata rows:"
  140. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.metadata.results.bindings | length | tostring | " - rows: " + .'
  141. fi
  142. section "Subject/path helpers"
  143. PATH_SUBJECT="http://world.eu.org/example1#Plant_cookie_kerosene_2026_3"
  144. TOOL_LAST_RESPONSE=""
  145. if assert_tool_ok "describe_subject(cookie path)" "{\"tool\":\"describe_subject\",\"input\":{\"subject_uri\":\"${PATH_SUBJECT}\",\"limit\":10}}"; then
  146. echo "Subject triples:"
  147. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - \(.predicate.value) -> \(.objectLabel.value // .object.value)"' || true
  148. fi
  149. TOOL_LAST_RESPONSE=""
  150. 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
  151. echo "Path traverse results:"
  152. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.result.results.bindings[] | " - " + (.n1Label.value // .n1.value) + " -> " + (.n2Label.value // .n2.value)' || true
  153. fi
  154. TOOL_LAST_RESPONSE=""
  155. 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
  156. echo "cloneOf usage count:"
  157. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.count.results.bindings[0].usageCount.value // "0" | " - " + .' || true
  158. echo "Sample bindings:"
  159. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.examples.results.bindings[] | " - " + (.subjectLabel.value // .subject.value) + " -> " + (.objectLabel.value // .object.value)' || true
  160. fi
  161. TOOL_LAST_RESPONSE=""
  162. if assert_tool_ok "batch_insert(test)" '{"tool":"batch_insert","input":{"ttl":"<http://example.org/plain#BatchPlant> <http://www.w3.org/2000/01/rdf-schema#label> \"batch helper\" .","graph":"http://world.eu.org/cannabis-breeding#test"}}'; then
  163. echo "Batch insert query:"
  164. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.query' | sed 's/^/ /'
  165. fi
  166. section "Summary"
  167. echo "Passed: $pass_count"
  168. echo "Failed: $fail_count"
  169. if [[ "$fail_count" -gt 0 ]]; then
  170. exit 1
  171. fi
  172. echo "All checks passed."