|
@@ -6,6 +6,9 @@
|
|
|
BASE="${1:-http://192.168.0.200:8420}"
|
|
BASE="${1:-http://192.168.0.200:8420}"
|
|
|
PASS=0
|
|
PASS=0
|
|
|
FAIL=0
|
|
FAIL=0
|
|
|
|
|
+LAST_JSON=""
|
|
|
|
|
+LAST_STATUS=""
|
|
|
|
|
+MEMORY_MARKER="mem0-test-case"
|
|
|
|
|
|
|
|
# Colours
|
|
# Colours
|
|
|
GREEN='\033[0;32m'
|
|
GREEN='\033[0;32m'
|
|
@@ -18,9 +21,13 @@ sep() { echo -e "\n${CYAN}${BOLD}━━━ $* ━━━${RESET}"; }
|
|
|
ok() { echo -e "${GREEN}✓ $*${RESET}"; ((PASS++)); }
|
|
ok() { echo -e "${GREEN}✓ $*${RESET}"; ((PASS++)); }
|
|
|
fail() { echo -e "${RED}✗ $*${RESET}"; ((FAIL++)); }
|
|
fail() { echo -e "${RED}✗ $*${RESET}"; ((FAIL++)); }
|
|
|
|
|
|
|
|
-# Run curl, pretty-print with jq, check HTTP 200
|
|
|
|
|
run() {
|
|
run() {
|
|
|
local label="$1"; shift
|
|
local label="$1"; shift
|
|
|
|
|
+ local expected="200"
|
|
|
|
|
+ if [[ "$1" =~ ^[0-9]{3}$ ]]; then
|
|
|
|
|
+ expected="$1"
|
|
|
|
|
+ shift
|
|
|
|
|
+ fi
|
|
|
echo -e "\n${BOLD}▶ $label${RESET}"
|
|
echo -e "\n${BOLD}▶ $label${RESET}"
|
|
|
local body
|
|
local body
|
|
|
body=$(curl -s -w "\n__STATUS__%{http_code}" "$@")
|
|
body=$(curl -s -w "\n__STATUS__%{http_code}" "$@")
|
|
@@ -31,134 +38,170 @@ run() {
|
|
|
|
|
|
|
|
echo "$json" | jq . 2>/dev/null || echo "$json"
|
|
echo "$json" | jq . 2>/dev/null || echo "$json"
|
|
|
|
|
|
|
|
- if [[ "$status" == "200" ]]; then
|
|
|
|
|
|
|
+ LAST_STATUS="$status"
|
|
|
|
|
+ LAST_JSON="$json"
|
|
|
|
|
+
|
|
|
|
|
+ if [[ "$status" == "$expected" ]]; then
|
|
|
ok "HTTP $status"
|
|
ok "HTTP $status"
|
|
|
else
|
|
else
|
|
|
- fail "HTTP $status"
|
|
|
|
|
|
|
+ fail "HTTP $status (expected $expected)"
|
|
|
fi
|
|
fi
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
-sep "HEALTH"
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
|
|
+assert_json() {
|
|
|
|
|
+ local label="$1" expr="$2"
|
|
|
|
|
+ if echo "$LAST_JSON" | jq -e "$expr" >/dev/null 2>&1; then
|
|
|
|
|
+ ok "$label"
|
|
|
|
|
+ else
|
|
|
|
|
+ fail "$label (jq: $expr)"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-run "GET /health" \
|
|
|
|
|
- "$BASE/health"
|
|
|
|
|
|
|
+assert_contains() {
|
|
|
|
|
+ local label="$1" substring="$2"
|
|
|
|
|
+ if echo "$LAST_JSON" | grep -qF "$substring"; then
|
|
|
|
|
+ ok "$label"
|
|
|
|
|
+ else
|
|
|
|
|
+ fail "$label (missing '$substring')"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
-sep "/memories (conversational — OpenClaw)"
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
|
|
+sep "HEALTH"
|
|
|
|
|
+run "GET /health" "$BASE/health"
|
|
|
|
|
+assert_json "health -> collections object" '.collections | type == "object"'
|
|
|
|
|
+assert_json "health -> prompts present" '.prompts | type == "object"'
|
|
|
|
|
+
|
|
|
|
|
+sep "DASHBOARD"
|
|
|
|
|
+run "GET /dashboard" "$BASE/dashboard"
|
|
|
|
|
+assert_contains "dashboard contains HTML" "<html"
|
|
|
|
|
|
|
|
|
|
+sep "/memories (conversational)"
|
|
|
run "POST /memories — plain text" \
|
|
run "POST /memories — plain text" \
|
|
|
-X POST "$BASE/memories" \
|
|
-X POST "$BASE/memories" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
- -d '{"text": "I love building AI agents and I prefer Python over JavaScript", "user_id": "testuser"}'
|
|
|
|
|
|
|
+ -d '{"text": "${MEMORY_MARKER} plain text entry for testing", "user_id": "testuser"}'
|
|
|
|
|
|
|
|
run "POST /memories — messages array" \
|
|
run "POST /memories — messages array" \
|
|
|
-X POST "$BASE/memories" \
|
|
-X POST "$BASE/memories" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
- -d '{
|
|
|
|
|
- "messages": [{"role": "user", "content": "I have been using Vim for 10 years and hate GUI editors"}],
|
|
|
|
|
- "user_id": "testuser"
|
|
|
|
|
- }'
|
|
|
|
|
|
|
+ -d '{"messages": [{"role": "user", "content": "${MEMORY_MARKER} message entry for testing"}], "user_id": "testuser"}'
|
|
|
|
|
+
|
|
|
|
|
+run "POST /memories/all" \
|
|
|
|
|
+ -X POST "$BASE/memories/all" \
|
|
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
|
|
+ -d '{"user_id": "testuser"}'
|
|
|
|
|
+assert_json "memories/all returns array" '.results | type == "array"'
|
|
|
|
|
|
|
|
-run "POST /memories/search — programming preferences" \
|
|
|
|
|
|
|
+run "POST /memories/search — capture marker" \
|
|
|
-X POST "$BASE/memories/search" \
|
|
-X POST "$BASE/memories/search" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
- -d '{"query": "programming preferences", "user_id": "testuser", "limit": 5}'
|
|
|
|
|
|
|
+ -d '{"query": "$MEMORY_MARKER", "user_id": "testuser", "limit": 5 }'
|
|
|
|
|
+assert_json "memories marker search returns array" '.results | type == "array"'
|
|
|
|
|
+
|
|
|
|
|
+MEMORY_IDS=$(echo "$LAST_JSON" | jq -r '.results[] | .id')
|
|
|
|
|
+if [[ -z "$MEMORY_IDS" ]]; then
|
|
|
|
|
+ fail "no test memory IDs captured"
|
|
|
|
|
+else
|
|
|
|
|
+ ok "captured conversational memory IDs"
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+run "POST /memories/search" \
|
|
|
|
|
+ -X POST "$BASE/memories/search" \
|
|
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
|
|
+ -d '{"query": "programming preferences", "user_id": "testuser", "limit": 5 }'
|
|
|
|
|
+assert_json "memories search returns array" '.results | type == "array"'
|
|
|
|
|
|
|
|
run "POST /memories/recent" \
|
|
run "POST /memories/recent" \
|
|
|
-X POST "$BASE/memories/recent" \
|
|
-X POST "$BASE/memories/recent" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
-d '{"user_id": "testuser", "limit": 5}'
|
|
-d '{"user_id": "testuser", "limit": 5}'
|
|
|
-
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
-sep "/knowledge (objective facts — book-ingestor)"
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
-
|
|
|
|
|
-run "POST /knowledge — Gesell with metadata (LLM extraction)" \
|
|
|
|
|
|
|
+assert_json "memories recent returns array" '.results | type == "array"'
|
|
|
|
|
+
|
|
|
|
|
+if [[ -n "$MEMORY_IDS" ]]; then
|
|
|
|
|
+ for id in $MEMORY_IDS; do
|
|
|
|
|
+ run "DELETE /memory/$id" \
|
|
|
|
|
+ -X DELETE "$BASE/memory/$id" \
|
|
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
|
|
+ -d '{"collection": "conversational"}'
|
|
|
|
|
+ done
|
|
|
|
|
+else
|
|
|
|
|
+ fail "conversational memory IDs missing before cleanup"
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+sep "/knowledge (knowledge base)"
|
|
|
|
|
+run "POST /knowledge — Gesell metadata" \
|
|
|
-X POST "$BASE/knowledge" \
|
|
-X POST "$BASE/knowledge" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
- -d '{
|
|
|
|
|
- "text": "Silvio Gesell proposed demurrage as a mechanism to discourage hoarding of currency. He described this in The Natural Economic Order published in 1916.",
|
|
|
|
|
- "user_id": "knowledge_base",
|
|
|
|
|
- "metadata": {"source_file": "gesell_neo.pdf", "chapter": 3, "page": 47}
|
|
|
|
|
- }'
|
|
|
|
|
|
|
+ -d '{"text": "Silvio Gesell proposed demurrage to discourage hoarding of currency.", "user_id": "knowledge_base", "metadata": {"source_file": "gesell_neo.pdf", "chapter": 3, "page": 47}}'
|
|
|
|
|
|
|
|
-run "POST /knowledge — MIDI verbatim (infer:false)" \
|
|
|
|
|
|
|
+run "POST /knowledge — MIDI verbatim" \
|
|
|
-X POST "$BASE/knowledge" \
|
|
-X POST "$BASE/knowledge" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
- -d '{
|
|
|
|
|
- "text": "MIDI SysEx messages use a 7-bit checksum computed as the twos complement of the sum of all data bytes.",
|
|
|
|
|
- "user_id": "knowledge_base",
|
|
|
|
|
- "infer": false,
|
|
|
|
|
- "metadata": {"source_file": "midi_spec.pdf", "chapter": 9, "page": 112}
|
|
|
|
|
- }'
|
|
|
|
|
|
|
+ -d '{"text": "MIDI SysEx uses a 7-bit checksum.", "user_id": "knowledge_base", "infer": false, "metadata": {"source_file": "midi_spec.pdf", "chapter": 9, "page": 112}}'
|
|
|
|
|
|
|
|
-run "POST /knowledge/search — Gesell demurrage (should NOT return MIDI)" \
|
|
|
|
|
|
|
+run "POST /knowledge/search — Gesell demurrage" \
|
|
|
-X POST "$BASE/knowledge/search" \
|
|
-X POST "$BASE/knowledge/search" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
-d '{"query": "Gesell demurrage", "user_id": "knowledge_base", "limit": 5}'
|
|
-d '{"query": "Gesell demurrage", "user_id": "knowledge_base", "limit": 5}'
|
|
|
|
|
+assert_json "knowledge search results array" '.results | type == "array"'
|
|
|
|
|
|
|
|
-run "POST /knowledge/search — free money currency hoarding" \
|
|
|
|
|
- -X POST "$BASE/knowledge/search" \
|
|
|
|
|
|
|
+run "POST /knowledge/recent" \
|
|
|
|
|
+ -X POST "$BASE/knowledge/recent" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
- -d '{"query": "free money currency hoarding", "user_id": "knowledge_base", "limit": 5}'
|
|
|
|
|
|
|
+ -d '{"user_id": "knowledge_base", "limit": 5}'
|
|
|
|
|
+assert_json "knowledge recent returns array" '.results | type == "array"'
|
|
|
|
|
|
|
|
-run "POST /knowledge/search — MIDI checksum (should NOT return Gesell)" \
|
|
|
|
|
- -X POST "$BASE/knowledge/search" \
|
|
|
|
|
|
|
+run "POST /knowledge/sources" \
|
|
|
|
|
+ -X POST "$BASE/knowledge/sources" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
- -d '{"query": "MIDI checksum SysEx", "user_id": "knowledge_base", "limit": 5}'
|
|
|
|
|
|
|
+ -d '{"user_id": "knowledge_base"}'
|
|
|
|
|
+assert_json "knowledge sources array" '.sources | type == "array"'
|
|
|
|
|
+assert_json "knowledge sources include Gesell" '.sources | any(.source_file == "gesell_neo.pdf")'
|
|
|
|
|
|
|
|
-run "POST /knowledge/recent" \
|
|
|
|
|
- -X POST "$BASE/knowledge/recent" \
|
|
|
|
|
|
|
+run "DELETE /knowledge/by-source — gesell_neo.pdf" \
|
|
|
|
|
+ -X DELETE "$BASE/knowledge/by-source" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
- -d '{"user_id": "knowledge_base", "limit": 5}'
|
|
|
|
|
|
|
+ -d '{"source_file": "gesell_neo.pdf", "user_id": "knowledge_base"}'
|
|
|
|
|
+assert_json "knowledge by-source removed entries" '.deleted | tonumber > 0'
|
|
|
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
-sep "/search (merged — both collections)"
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
|
|
+run "DELETE /knowledge/by-source — midi_spec.pdf" \
|
|
|
|
|
+ -X DELETE "$BASE/knowledge/by-source" \
|
|
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
|
|
+ -d '{"source_file": "midi_spec.pdf", "user_id": "knowledge_base"}'
|
|
|
|
|
+assert_json "knowledge by-source removed MIDI entries" '.deleted | tonumber > 0'
|
|
|
|
|
|
|
|
-run "POST /search — Python programming (expect _source tags on results)" \
|
|
|
|
|
|
|
+sep "/search (merged)"
|
|
|
|
|
+run "POST /search — Python programming" \
|
|
|
-X POST "$BASE/search" \
|
|
-X POST "$BASE/search" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
-d '{"query": "Python programming", "user_id": "testuser", "limit": 8}'
|
|
-d '{"query": "Python programming", "user_id": "testuser", "limit": 8}'
|
|
|
|
|
+assert_json "merged search results array" '.results | type == "array"'
|
|
|
|
|
+assert_json "merged search items have _source" 'if (.results | length) == 0 then true else (.results[] | has("_source")) end'
|
|
|
|
|
|
|
|
-run "POST /search — Gesell economic theory (cross-collection)" \
|
|
|
|
|
|
|
+run "POST /search — Gesell economic theory" \
|
|
|
-X POST "$BASE/search" \
|
|
-X POST "$BASE/search" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
-d '{"query": "Gesell economic theory", "user_id": "knowledge_base", "limit": 5}'
|
|
-d '{"query": "Gesell economic theory", "user_id": "knowledge_base", "limit": 5}'
|
|
|
|
|
+assert_json "merged search results array (Gesell)" '.results | type == "array"'
|
|
|
|
|
+assert_json "merged Gesell results tag _source" 'if (.results | length) == 0 then true else (.results[] | has("_source")) end'
|
|
|
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
sep "ERROR HANDLING"
|
|
sep "ERROR HANDLING"
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
-
|
|
|
|
|
-run "POST /memories — missing text/messages (expect 400)" \
|
|
|
|
|
|
|
+run "POST /memories — missing text" 400 \
|
|
|
-X POST "$BASE/memories" \
|
|
-X POST "$BASE/memories" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
-d '{"user_id": "testuser"}'
|
|
-d '{"user_id": "testuser"}'
|
|
|
|
|
|
|
|
-run "POST /memories — infer:false ignored on /memories (no verbatim)" \
|
|
|
|
|
|
|
+run "POST /memories — infer:false ignored" \
|
|
|
-X POST "$BASE/memories" \
|
|
-X POST "$BASE/memories" \
|
|
|
-H "Content-Type: application/json" \
|
|
-H "Content-Type: application/json" \
|
|
|
-d '{"text": "This should still go through LLM extraction", "user_id": "testuser", "infer": false}'
|
|
-d '{"text": "This should still go through LLM extraction", "user_id": "testuser", "infer": false}'
|
|
|
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
-sep "DELETE (comment out if you want to keep test data)"
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
-
|
|
|
|
|
-# Uncomment to clean up after testing:
|
|
|
|
|
-# run "DELETE /memories — testuser" \
|
|
|
|
|
-# -X DELETE "$BASE/memories" \
|
|
|
|
|
-# -H "Content-Type: application/json" \
|
|
|
|
|
-# -d '{"filter": {"user_id": "testuser"}}'
|
|
|
|
|
-#
|
|
|
|
|
-# run "DELETE /knowledge — knowledge_base" \
|
|
|
|
|
-# -X DELETE "$BASE/knowledge" \
|
|
|
|
|
-# -H "Content-Type: application/json" \
|
|
|
|
|
-# -d '{"filter": {"user_id": "knowledge_base"}}'
|
|
|
|
|
-
|
|
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
|
|
+run "POST /search — empty query" \
|
|
|
|
|
+ -X POST "$BASE/search" \
|
|
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
|
|
+ -d '{"query": "", "user_id": "testuser"}'
|
|
|
|
|
+assert_json "empty search returns results array" '.results | type == "array"'
|
|
|
|
|
+assert_json "empty search returns zero results" '.results | length == 0'
|
|
|
|
|
+
|
|
|
sep "RESULTS"
|
|
sep "RESULTS"
|
|
|
-# ─────────────────────────────────────────────
|
|
|
|
|
-echo -e "\n${BOLD}Passed: ${GREEN}$PASS${RESET} ${BOLD}Failed: ${RED}$FAIL${RESET}\n"
|
|
|
|
|
|
|
+echo -e "\n${BOLD}Passed: ${GREEN}$PASS${RESET} ${BOLD}Failed: ${RED}$FAIL${RESET}\n"
|