test.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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="${EXAMPLE_GRAPH:-http://example.org/catalog#test}"
  14. DERIVED_PROPERTY="http://example.org/relations#derivedFrom"
  15. SOURCE_BATCH_PROPERTY="http://example.org/relations#sourceBatch"
  16. ORIGIN_BATCH_PROPERTY="http://example.org/relations#originBatch"
  17. CATEGORY_TYPE="http://example.org/catalog#Category"
  18. ROOT_ITEM="http://example.org/catalog#Item_Prototype"
  19. PATH_SUBJECT="http://example.org/catalog#Item_Prime"
  20. SAMPLE_FIXTURE="catalog_fixture.ttl"
  21. : "${MCP_ALLOW_EXAMPLE_LOAD:=true}"
  22. if ! command -v jq >/dev/null 2>&1; then
  23. echo "ERROR: jq is required for test output parsing."
  24. exit 1
  25. fi
  26. pass_count=0
  27. fail_count=0
  28. section() {
  29. echo
  30. echo "============================================================"
  31. echo "$1"
  32. echo "============================================================"
  33. }
  34. pass() {
  35. pass_count=$((pass_count + 1))
  36. echo "✅ PASS: $1"
  37. }
  38. fail() {
  39. fail_count=$((fail_count + 1))
  40. echo "❌ FAIL: $1"
  41. echo " detail: $2"
  42. }
  43. call_mcp() {
  44. local payload="$1"
  45. curl -sS -X POST "$BASE_URL/rpc" \
  46. -H "Content-Type: application/json" \
  47. -d "$payload"
  48. }
  49. assert_tool_ok() {
  50. local label="$1"
  51. local payload="$2"
  52. local response
  53. response="$(call_mcp "$payload")" || {
  54. fail "$label" "HTTP request failed"
  55. return 1
  56. }
  57. if ! echo "$response" | jq -e . >/dev/null 2>&1; then
  58. fail "$label" "Non-JSON response: $response"
  59. return 1
  60. fi
  61. local status
  62. status="$(echo "$response" | jq -r '.status // empty')"
  63. if [[ "$status" != "ok" ]]; then
  64. local detail
  65. detail="$(echo "$response" | jq -r '.detail // .error // "unknown error"')"
  66. fail "$label" "$detail"
  67. return 1
  68. fi
  69. pass "$label"
  70. TOOL_LAST_RESPONSE="$response"
  71. return 0
  72. }
  73. section "Load sample dataset"
  74. if [[ "${MCP_ALLOW_EXAMPLE_LOAD:-false}" == "true" ]]; then
  75. payload=$(jq -n --arg file "$SAMPLE_FIXTURE" --arg graph "$TEST_GRAPH" '{"tool":"load_examples","input":{"files":[$file],"graph":$graph}}')
  76. TOOL_LAST_RESPONSE=""
  77. if assert_tool_ok "load_examples" "$payload"; then
  78. echo "Loaded fixtures:"
  79. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.loaded[] | " - " + (.file + " → " + .graph)' || true
  80. fi
  81. else
  82. echo "MCP_ALLOW_EXAMPLE_LOAD not true; skipping sample dataset load."
  83. fi
  84. section "Health check"
  85. root_json="$(curl -sS "$BASE_URL/")"
  86. echo "$root_json" | jq '{status, virtuoso, tools, guardrails}'
  87. if [[ "$(echo "$root_json" | jq -r '.status // empty')" == "MCP server running" ]]; then
  88. pass "root endpoint"
  89. else
  90. fail "root endpoint" "unexpected status"
  91. fi
  92. section "Read-only tools"
  93. TOOL_LAST_RESPONSE=""
  94. if assert_tool_ok "list_graphs" '{"tool":"list_graphs","input":{}}'; then
  95. echo "Graphs returned:"
  96. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].g.value' | sed 's/^/ - /' || true
  97. fi
  98. section "Update path (fabricated triples)"
  99. TOOL_LAST_RESPONSE=""
  100. payload=$(jq -n --arg subject "http://example.org/plain#TestItem1" --arg predicate "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" --arg object "http://example.org/plain#Specimen" --arg graph "$TEST_GRAPH" '{"tool":"insert_triple","input":{"subject":$subject,"predicate":$predicate,"object":$object,"object_type":"uri","graph":$graph}}')
  101. if assert_tool_ok "insert_triple" "$payload"; then
  102. echo "Inserted query preview:"
  103. echo "$TOOL_LAST_RESPONSE" | jq -r '.query' | sed 's/^/ /'
  104. fi
  105. TOOL_LAST_RESPONSE=""
  106. UPDATE_QUERY=$(cat <<'EOF'
  107. INSERT DATA {
  108. GRAPH <http://example.org/catalog#test> {
  109. <http://example.org/plain#UpdateToolItem> <http://www.w3.org/2000/01/rdf-schema#label> "update tool item" .
  110. }
  111. }
  112. EOF
  113. )
  114. payload=$(jq -n --arg query "$UPDATE_QUERY" '{"tool":"sparql_update","input":{"query":$query}}')
  115. if assert_tool_ok "sparql_update (INSERT DATA)" "$payload"; then
  116. echo "Update query preview:"
  117. echo "$TOOL_LAST_RESPONSE" | jq -r '.query' | sed 's/^/ /'
  118. fi
  119. BAD_UPDATE=$(jq -n --arg query "SELECT * WHERE { ?s ?p ?o }" '{"tool":"sparql_update","input":{"query":$query}}')
  120. BAD_RESPONSE="$(call_mcp "$BAD_UPDATE")"
  121. if echo "$BAD_RESPONSE" | jq -e '.status == "ok"' >/dev/null 2>&1; then
  122. fail "sparql_update rejects SELECT" "unexpected success"
  123. else
  124. echo "Confirmed sparql_update rejects read-only queries."
  125. fi
  126. EXAMPLE_TTL=$(cat <<'EOF'
  127. @prefix ex: <http://example.org/plain#> .
  128. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
  129. ex:PlainItem a ex:Specimen ;
  130. rdfs:label "Plain test item" .
  131. EOF
  132. )
  133. payload=$(jq -n --arg ttl "$EXAMPLE_TTL" --arg graph "$TEST_GRAPH" '{"tool":"batch_insert","input":{"ttl":$ttl,"graph":$graph}}')
  134. TOOL_LAST_RESPONSE=""
  135. if assert_tool_ok "batch_insert (fabricated TTL)" "$payload"; then
  136. echo "Batch insert query:"
  137. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.query' | sed 's/^/ /'
  138. fi
  139. section "Helper retrieval checks"
  140. TOOL_LAST_RESPONSE=""
  141. payload=$(jq -n --arg type_uri "$CATEGORY_TYPE" --argjson limit 5 '{"tool":"get_entities_by_type","input":{"type_uri":$type_uri,"limit":$limit}}')
  142. if assert_tool_ok "get_entities_by_type(Category)" "$payload"; then
  143. echo "Entity IRIs:"
  144. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].s.value' | sed 's/^/ - /' || true
  145. fi
  146. TOOL_LAST_RESPONSE=""
  147. payload=$(jq -n --arg term "Variant" --argjson limit 5 '{"tool":"search_label","input":{"term":$term,"limit":$limit}}')
  148. if assert_tool_ok "search_label(term=Variant)" "$payload"; then
  149. echo "Label hits:"
  150. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + (.label.value // "<no label>") + " (" + .s.value + ")"' || true
  151. fi
  152. TOOL_LAST_RESPONSE=""
  153. payload=$(jq -n --arg subject_uri "$PATH_SUBJECT" --argjson limit 10 '{"tool":"get_predicates_for_subject","input":{"subject_uri":$subject_uri,"limit":$limit}}')
  154. if assert_tool_ok "get_predicates_for_subject(Prime Item)" "$payload"; then
  155. echo "Predicates found:"
  156. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].p.value' | sed 's/^/ - /' || true
  157. fi
  158. TOOL_LAST_RESPONSE=""
  159. payload=$(jq -n --arg subject_uri "$PATH_SUBJECT" '{"tool":"get_labels_for_subject","input":{"subject_uri":$subject_uri}}')
  160. if assert_tool_ok "get_labels_for_subject(Prime Item)" "$payload"; then
  161. echo "Subject labels:"
  162. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[].label.value' | sed 's/^/ - /' || true
  163. fi
  164. section "Relationship inspection"
  165. TOOL_LAST_RESPONSE=""
  166. payload=$(jq -n --arg subject_uri "$ROOT_ITEM" --arg property_uri "$DERIVED_PROPERTY" --arg direction "incoming" --argjson limit 20 '{"tool":"traverse_property","input":{"subject_uri":$subject_uri,"property_uri":$property_uri,"direction":$direction,"limit":$limit}}')
  167. if assert_tool_ok "traverse_property(derivedFrom incoming)" "$payload"; then
  168. echo "Items derived from the prototype:"
  169. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + (.neighbor.value) + " (label: " + (.label.value // "<no label>") + ")"'
  170. fi
  171. section "Ontology discovery"
  172. TOOL_LAST_RESPONSE=""
  173. if assert_tool_ok "list_classes(term=cycle)" '{"tool":"list_classes","input":{"term":"cycle","limit":10}}'; then
  174. echo "Class hits:"
  175. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + .class.value + " (" + (.label.value // "<no label>") + ")"' || true
  176. fi
  177. TOOL_LAST_RESPONSE=""
  178. if assert_tool_ok "list_properties(term=derived)" '{"tool":"list_properties","input":{"term":"derived","limit":10}}'; then
  179. echo "Property hits:"
  180. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + .property.value + " (" + (.label.value // "<no label>") + ")"' || true
  181. fi
  182. TOOL_LAST_RESPONSE=""
  183. payload=$(jq -n --arg property_uri "$DERIVED_PROPERTY" --argjson limit 5 '{"tool":"describe_property","input":{"property_uri":$property_uri,"usage_limit":$limit}}')
  184. if assert_tool_ok "describe_property(derivedFrom)" "$payload"; then
  185. echo "derivedFrom metadata rows:"
  186. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.metadata.results.bindings | length | tostring | " - rows: " + .'
  187. fi
  188. section "Subject/path helpers"
  189. TOOL_LAST_RESPONSE=""
  190. payload=$(jq -n --arg subject_uri "$PATH_SUBJECT" --argjson limit 10 '{"tool":"describe_subject","input":{"subject_uri":$subject_uri,"limit":$limit}}')
  191. if assert_tool_ok "describe_subject(Prime Item)" "$payload"; then
  192. echo "Subject triples:"
  193. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.results.bindings[] | " - " + .predicate.value + " -> " + (.objectLabel.value // .object.value)' || true
  194. fi
  195. TOOL_LAST_RESPONSE=""
  196. payload=$(jq -n --arg subject_uri "$PATH_SUBJECT" --arg property1 "$SOURCE_BATCH_PROPERTY" --arg property2 "$ORIGIN_BATCH_PROPERTY" --argjson limit 5 '{"tool":"path_traverse","input":{"subject_uri":$subject_uri,"property_path":[$property1,$property2],"limit":$limit}}')
  197. if assert_tool_ok "path_traverse(batch lineage)" "$payload"; then
  198. echo "Path traverse results:"
  199. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.result.results.bindings[] | " - " + (.n1Label.value // .n1.value) + " -> " + (.n2Label.value // .n2.value)' || true
  200. fi
  201. TOOL_LAST_RESPONSE=""
  202. payload=$(jq -n --arg property_uri "$DERIVED_PROPERTY" --argjson examples_limit 3 '{"tool":"property_usage_statistics","input":{"property_uri":$property_uri,"examples_limit":$examples_limit}}')
  203. if assert_tool_ok "property_usage_statistics" "$payload"; then
  204. echo "derivedFrom usage count:"
  205. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.count.results.bindings[0].usageCount.value // "0" | " - " + .'
  206. echo "Sample bindings:"
  207. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.examples.results.bindings[] | " - " + (.subjectLabel.value // .subject.value) + " -> " + (.objectLabel.value // .object.value)' || true
  208. fi
  209. TOOL_LAST_RESPONSE=""
  210. payload=$(jq -n --arg ttl "<http://example.org/plain#BatchItem> <http://www.w3.org/2000/01/rdf-schema#label> \"batch helper\" ." --arg graph "$TEST_GRAPH" '{"tool":"batch_insert","input":{"ttl":$ttl,"graph":$graph}}')
  211. if assert_tool_ok "batch_insert(test)" "$payload"; then
  212. echo "Batch insert query:"
  213. echo "$TOOL_LAST_RESPONSE" | jq -r '.result.query' | sed 's/^/ /'
  214. fi
  215. section "Summary"
  216. echo "Passed: $pass_count"
  217. echo "Failed: $fail_count"
  218. if [[ "$fail_count" -gt 0 ]]; then
  219. exit 1
  220. fi
  221. echo "All checks passed."