499 lines
15 KiB
Bash
Executable File
499 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
||
# =================================================================
|
||
# 织忆 (MemoryWeave) 端到端集成测试
|
||
# =================================================================
|
||
# API base: http://localhost:7821
|
||
# API Key: zhiyi-dev-key-2026
|
||
# =================================================================
|
||
|
||
# ── 配置 ──────────────────────────────────────────────────────
|
||
API_BASE="http://localhost:7821"
|
||
API_KEY="zhiyi-dev-key-2026"
|
||
CLI="/home/muc/.local/bin/zhiyi-cli"
|
||
NAMESPACE="test-integration"
|
||
AGENT_ID="test-agent-$$"
|
||
|
||
# 颜色输出
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# 测试计数器
|
||
TESTS_RUN=0
|
||
TESTS_PASSED=0
|
||
TESTS_FAILED=0
|
||
|
||
# ── 辅助函数 ──────────────────────────────────────────────────
|
||
|
||
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||
log_pass() { echo -e "${GREEN}[PASS]${NC} $*"; TESTS_PASSED=$((TESTS_PASSED+1)); }
|
||
log_fail() { echo -e "${RED}[FAIL]${NC} $*" >&2; TESTS_FAILED=$((TESTS_FAILED+1)); }
|
||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
|
||
header() { echo ""; echo "══════════════════════════════════════"; echo " $*"; echo "══════════════════════════════════════"; }
|
||
|
||
# API 调用封装(不使用 -f,允许非 2xx 响应)
|
||
api_get() {
|
||
local path="$1"
|
||
curl -s -H "X-API-Key: ${API_KEY}" "${API_BASE}${path}" 2>&1
|
||
}
|
||
|
||
api_post() {
|
||
local path="$1"
|
||
local body="$2"
|
||
curl -s -H "X-API-Key: ${API_KEY}" \
|
||
-H "Content-Type: application/json" \
|
||
-d "${body}" \
|
||
"${API_BASE}${path}" 2>&1
|
||
}
|
||
|
||
# 断言函数
|
||
assert_eq() {
|
||
local expected="$1"
|
||
local actual="$2"
|
||
local msg="$3"
|
||
TESTS_RUN=$((TESTS_RUN+1))
|
||
if [[ "$expected" == "$actual" ]]; then
|
||
log_pass "${msg}"
|
||
return 0
|
||
else
|
||
log_fail "${msg} - expected: '${expected}', got: '${actual}'"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
assert_contains() {
|
||
local haystack="$1"
|
||
local needle="$2"
|
||
local msg="$3"
|
||
TESTS_RUN=$((TESTS_RUN+1))
|
||
if echo "$haystack" | grep -q "$needle"; then
|
||
log_pass "${msg}"
|
||
return 0
|
||
else
|
||
log_fail "${msg} - '$needle' not found in response"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
assert_not_empty() {
|
||
local value="$1"
|
||
local msg="$2"
|
||
TESTS_RUN=$((TESTS_RUN+1))
|
||
if [[ -n "$value" ]]; then
|
||
log_pass "${msg}"
|
||
return 0
|
||
else
|
||
log_fail "${msg} - empty value"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
assert_json_valid() {
|
||
local json_str="$1"
|
||
local msg="$2"
|
||
TESTS_RUN=$((TESTS_RUN+1))
|
||
if echo "$json_str" | python3 -c "import sys,json; json.load(sys.stdin); print('ok')" > /dev/null 2>&1; then
|
||
log_pass "${msg}"
|
||
return 0
|
||
else
|
||
log_fail "${msg} - invalid JSON"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ── SETUP ─────────────────────────────────────────────────────
|
||
|
||
setup() {
|
||
log_info "执行测试前检查..."
|
||
|
||
# 检查服务是否运行
|
||
local health_resp
|
||
health_resp=$(curl -s "${API_BASE}/health" 2>&1)
|
||
if ! echo "$health_resp" | python3 -c "import sys,json; json.load(sys.stdin)" > /dev/null 2>&1; then
|
||
log_fail "服务未运行,请先启动 zhiyid (health check failed)"
|
||
exit 1
|
||
fi
|
||
|
||
# 检查 CLI 是否存在
|
||
if [[ ! -x "$CLI" ]]; then
|
||
log_warn "CLI 未找到或不可执行: $CLI"
|
||
fi
|
||
|
||
log_info "SETUP 完成"
|
||
}
|
||
|
||
# ── TEARDOWN ──────────────────────────────────────────────────
|
||
|
||
teardown() {
|
||
log_info "清理测试数据..."
|
||
log_info "TEARDOWN 完成"
|
||
}
|
||
|
||
# ── 测试用例 ──────────────────────────────────────────────────
|
||
|
||
test_health() {
|
||
header "测试: 健康检查 /health"
|
||
local resp
|
||
resp=$(api_get "/health")
|
||
|
||
assert_contains "$resp" '"status":"ok"' "health 端点返回 ok 状态"
|
||
assert_contains "$resp" '"service":"zhiyid"' "health 端点包含服务名"
|
||
}
|
||
|
||
test_stats() {
|
||
header "测试: 统计接口 /api/v1/stats"
|
||
local resp
|
||
resp=$(api_get "/api/v1/stats")
|
||
|
||
assert_contains "$resp" '"total_memories"' "stats 包含记忆总数字段"
|
||
assert_contains "$resp" '"total_episodes"' "stats 包含 episode 总数字段"
|
||
assert_contains "$resp" '"backend"' "stats 包含后端类型字段"
|
||
|
||
log_info "stats 响应: $(echo $resp | head -c 200)..."
|
||
}
|
||
|
||
test_graph_stats() {
|
||
header "测试: 图谱统计 /api/v1/graph/stats"
|
||
local resp
|
||
resp=$(api_get "/api/v1/graph/stats")
|
||
|
||
assert_contains "$resp" '"node_count"' "graph/stats 包含节点数"
|
||
assert_contains "$resp" '"edge_count"' "graph/stats 包含边数"
|
||
}
|
||
|
||
test_commit_recall_feedback() {
|
||
header "测试: commit -> recall -> feedback 完整链路"
|
||
|
||
# Step 1: Commit
|
||
log_info "步骤1: 提交记忆 (commit)"
|
||
local unique_content="集成测试记忆 $(date +%s) - 随机内容: $RANDOM"
|
||
local commit_resp
|
||
commit_resp=$(api_post "/api/v1/commit" "{
|
||
\"agent_id\": \"${AGENT_ID}\",
|
||
\"namespace\": \"${NAMESPACE}\",
|
||
\"content\": \"${unique_content}\",
|
||
\"category\": \"test\"
|
||
}")
|
||
|
||
log_info "commit 响应: $commit_resp"
|
||
assert_contains "$commit_resp" '"status"' "commit 包含状态字段"
|
||
|
||
# 提取 memory_id(如果存在)
|
||
local memory_id=""
|
||
if echo "$commit_resp" | grep -q '"memory_id"'; then
|
||
memory_id=$(echo "$commit_resp" | grep -o '"memory_id":"[^"]*"' | cut -d'"' -f4 | head -1)
|
||
fi
|
||
|
||
local episode_id=""
|
||
if echo "$commit_resp" | grep -q '"episode_id"'; then
|
||
episode_id=$(echo "$commit_resp" | grep -o '"episode_id":"[^"]*"' | cut -d'"' -f4 | head -1)
|
||
fi
|
||
|
||
log_info "获取到 episode_id: ${episode_id}, memory_id: ${memory_id}"
|
||
assert_not_empty "$episode_id" "commit 返回 episode_id"
|
||
|
||
# 等待索引更新
|
||
sleep 1
|
||
|
||
# Step 2: Recall
|
||
log_info "步骤2: 检索记忆 (recall)"
|
||
local recall_resp
|
||
recall_resp=$(api_post "/api/v1/recall" "{
|
||
\"query\": \"集成测试记忆\",
|
||
\"namespace\": \"${NAMESPACE}\",
|
||
\"top_k\": 10
|
||
}")
|
||
|
||
log_info "recall 响应前200字符: $(echo $recall_resp | head -c 200)..."
|
||
assert_contains "$recall_resp" '"results"' "recall 包含 results 字段"
|
||
assert_contains "$recall_resp" '"count"' "recall 包含 count 字段"
|
||
|
||
# 尝试从 recall 结果中提取 memory_id
|
||
if [[ -z "$memory_id" ]]; then
|
||
memory_id=$(echo "$recall_resp" | grep -o '"id":"mem_[^"]*"' | head -1 | cut -d'"' -f4)
|
||
log_info "从 recall 结果提取 memory_id: ${memory_id}"
|
||
fi
|
||
|
||
# Step 3: Feedback
|
||
if [[ -n "$memory_id" ]]; then
|
||
log_info "步骤3: 反馈 (feedback)"
|
||
local feedback_resp
|
||
feedback_resp=$(api_post "/api/v1/feedback" "{
|
||
\"memory_id\": \"${memory_id}\",
|
||
\"useful\": true
|
||
}")
|
||
|
||
log_info "feedback 响应: $feedback_resp"
|
||
assert_contains "$feedback_resp" '"status"' "feedback 包含状态字段"
|
||
else
|
||
log_warn "无法获取 memory_id,跳过 feedback 测试"
|
||
fi
|
||
|
||
log_info "commit -> recall -> feedback 链路测试完成"
|
||
}
|
||
|
||
test_navigate() {
|
||
header "测试: 图谱导航 /api/v1/graph/navigate"
|
||
|
||
# 先创建一条可导航的记忆
|
||
local nav_entity="测试实体_$$"
|
||
api_post "/api/v1/commit" "{
|
||
\"agent_id\": \"${AGENT_ID}\",
|
||
\"namespace\": \"${NAMESPACE}\",
|
||
\"content\": \"${nav_entity} 是一个测试实体,用于图谱导航测试\",
|
||
\"category\": \"test\"
|
||
}" > /dev/null 2>&1
|
||
|
||
sleep 1
|
||
|
||
# 执行导航查询
|
||
local nav_resp
|
||
nav_resp=$(api_post "/api/v1/graph/navigate" "{
|
||
\"entity\": \"${nav_entity}\",
|
||
\"max_hops\": 2,
|
||
\"namespace\": \"${NAMESPACE}\"
|
||
}")
|
||
|
||
log_info "navigate 响应: $(echo $nav_resp | head -c 200)..."
|
||
assert_json_valid "$nav_resp" "navigate 返回有效 JSON"
|
||
|
||
# 双向导航测试
|
||
local nav_bi_resp
|
||
nav_bi_resp=$(api_post "/api/v1/graph/navigate" "{
|
||
\"source\": \"${nav_entity}\",
|
||
\"target\": \"另一个实体\",
|
||
\"max_hops\": 2,
|
||
\"namespace\": \"${NAMESPACE}\"
|
||
}")
|
||
|
||
assert_json_valid "$nav_bi_resp" "双向 navigate 返回有效 JSON"
|
||
}
|
||
|
||
test_concurrent() {
|
||
header "测试: 并发请求"
|
||
|
||
local pids=()
|
||
local outputs=()
|
||
|
||
# 同时发起 5 个并发请求
|
||
log_info "发起 5 个并发 commit 请求..."
|
||
|
||
for i in {1..5}; do
|
||
(
|
||
api_post "/api/v1/commit" "{
|
||
\"agent_id\": \"${AGENT_ID}\",
|
||
\"namespace\": \"${NAMESPACE}\",
|
||
\"content\": \"并发测试记忆 ${i} - $(date +%s%N)\",
|
||
\"category\": \"concurrent-test\"
|
||
}" 2>&1
|
||
) &
|
||
pids+=($!)
|
||
done
|
||
|
||
# 等待所有请求完成
|
||
local all_ok=true
|
||
for pid in "${pids[@]}"; do
|
||
if ! wait "$pid"; then
|
||
all_ok=false
|
||
fi
|
||
done
|
||
|
||
TESTS_RUN=$((TESTS_RUN+1))
|
||
if $all_ok; then
|
||
log_pass "5 个并发 commit 请求全部成功"
|
||
else
|
||
log_fail "部分并发请求失败"
|
||
fi
|
||
|
||
# 测试并发 recall
|
||
log_info "发起 5 个并发 recall 请求..."
|
||
pids=()
|
||
for i in {1..5}; do
|
||
(
|
||
api_post "/api/v1/recall" "{
|
||
\"query\": \"并发测试\",
|
||
\"namespace\": \"${NAMESPACE}\",
|
||
\"top_k\": 5
|
||
}" 2>&1
|
||
) &
|
||
pids+=($!)
|
||
done
|
||
|
||
all_ok=true
|
||
for pid in "${pids[@]}"; do
|
||
if ! wait "$pid"; then
|
||
all_ok=false
|
||
fi
|
||
done
|
||
|
||
TESTS_RUN=$((TESTS_RUN+1))
|
||
if $all_ok; then
|
||
log_pass "5 个并发 recall 请求全部成功"
|
||
else
|
||
log_fail "部分并发 recall 请求失败"
|
||
fi
|
||
}
|
||
|
||
test_batch_commit() {
|
||
header "测试: 批量提交 /api/v1/batch-commit"
|
||
|
||
local batch_resp
|
||
batch_resp=$(api_post "/api/v1/batch-commit" "{
|
||
\"namespace\": \"${NAMESPACE}\",
|
||
\"items\": [
|
||
{\"content\": \"批量测试项1\", \"tags\": [\"test\", \"batch\"]},
|
||
{\"content\": \"批量测试项2\", \"tags\": [\"test\", \"batch\"]},
|
||
{\"content\": \"批量测试项3\", \"tags\": [\"test\", \"batch\"]}
|
||
]
|
||
}")
|
||
|
||
log_info "batch-commit 响应: $(echo $batch_resp | head -c 200)..."
|
||
|
||
assert_json_valid "$batch_resp" "batch-commit 返回有效 JSON"
|
||
}
|
||
|
||
test_cli_consistency() {
|
||
header "测试: CLI 与 API 一致性"
|
||
|
||
if [[ ! -x "$CLI" ]]; then
|
||
log_warn "CLI 不可用,跳过 CLI 一致性测试"
|
||
return
|
||
fi
|
||
|
||
# 测试 CLI stats 与 API stats 一致性
|
||
log_info "比较 CLI stats 与 API stats..."
|
||
|
||
local cli_stats
|
||
cli_stats=$("$CLI" -url "${API_BASE}" -key "${API_KEY}" -n "${NAMESPACE}" stats 2>&1)
|
||
|
||
log_info "CLI stats 输出: $cli_stats"
|
||
|
||
TESTS_RUN=$((TESTS_RUN+1))
|
||
if [[ $? -eq 0 ]]; then
|
||
log_pass "CLI stats 命令执行成功"
|
||
else
|
||
log_fail "CLI stats 命令执行失败"
|
||
fi
|
||
|
||
# 测试 CLI recall 与 API recall
|
||
log_info "比较 CLI recall 与 API recall..."
|
||
local cli_recall
|
||
cli_recall=$("$CLI" -url "${API_BASE}" -key "${API_KEY}" -n "${NAMESPACE}" recall "测试" 2>&1)
|
||
|
||
local api_recall
|
||
api_recall=$(api_post "/api/v1/recall" "{
|
||
\"query\": \"测试\",
|
||
\"namespace\": \"${NAMESPACE}\",
|
||
\"top_k\": 10
|
||
}")
|
||
|
||
log_info "CLI recall 输出前100字符: $(echo $cli_recall | head -c 100)..."
|
||
log_info "API recall 输出前100字符: $(echo $api_recall | head -c 100)..."
|
||
|
||
# API recall 应返回有效 JSON
|
||
assert_json_valid "$api_recall" "CLI 和 API recall 都返回有效响应"
|
||
}
|
||
|
||
test_recall_debug() {
|
||
header "测试: Recall 诊断接口 /api/v1/recall/debug"
|
||
|
||
local debug_resp
|
||
debug_resp=$(api_post "/api/v1/recall/debug" "{
|
||
\"query\": \"集成测试\",
|
||
\"namespace\": \"${NAMESPACE}\",
|
||
\"top_k\": 5
|
||
}")
|
||
|
||
log_info "recall/debug 响应: $(echo $debug_resp | head -c 200)..."
|
||
|
||
assert_json_valid "$debug_resp" "recall/debug 返回有效 JSON"
|
||
assert_contains "$debug_resp" '"steps"' "recall/debug 包含诊断步骤"
|
||
assert_contains "$debug_resp" '"total_ms"' "recall/debug 包含耗时信息"
|
||
}
|
||
|
||
test_pagerank() {
|
||
header "测试: PageRank /api/v1/graph/pagerank"
|
||
|
||
local pr_resp
|
||
pr_resp=$(api_get "/api/v1/graph/pagerank")
|
||
|
||
log_info "pagerank 响应: $(echo $pr_resp | head -c 200)..."
|
||
|
||
assert_json_valid "$pr_resp" "pagerank 返回有效 JSON"
|
||
}
|
||
|
||
test_distill_quota() {
|
||
header "测试: 蒸馏配额 /api/v1/distill/quota"
|
||
|
||
local quota_resp
|
||
quota_resp=$(api_get "/api/v1/distill/quota")
|
||
|
||
log_info "distill/quota 响应: $quota_resp"
|
||
|
||
assert_json_valid "$quota_resp" "distill/quota 返回有效 JSON"
|
||
}
|
||
|
||
test_distill_status() {
|
||
header "测试: 蒸馏状态 /api/v1/distill/status"
|
||
|
||
local status_resp
|
||
status_resp=$(api_get "/api/v1/distill/status")
|
||
|
||
log_info "distill/status 响应: $status_resp"
|
||
|
||
assert_json_valid "$status_resp" "distill/status 返回有效 JSON"
|
||
}
|
||
|
||
# ── 运行所有测试 ──────────────────────────────────────────────
|
||
|
||
main() {
|
||
echo ""
|
||
echo "╔════════════════════════════════════════════════════════╗"
|
||
echo "║ 织忆 (MemoryWeave) 端到端集成测试 ║"
|
||
echo "║ API: ${API_BASE} ║"
|
||
echo "╚════════════════════════════════════════════════════════╝"
|
||
|
||
setup
|
||
|
||
# 执行所有测试
|
||
test_health
|
||
test_stats
|
||
test_graph_stats
|
||
test_commit_recall_feedback
|
||
test_navigate
|
||
test_concurrent
|
||
test_batch_commit
|
||
test_recall_debug
|
||
test_pagerank
|
||
test_distill_quota
|
||
test_distill_status
|
||
test_cli_consistency
|
||
|
||
teardown
|
||
|
||
# 输出测试总结
|
||
echo ""
|
||
echo "══════════════════════════════════════"
|
||
echo " 测试总结"
|
||
echo "══════════════════════════════════════"
|
||
echo -e " 运行: ${TESTS_RUN}"
|
||
echo -e " 通过: ${GREEN}${TESTS_PASSED}${NC}"
|
||
echo -e " 失败: ${RED}${TESTS_FAILED}${NC}"
|
||
echo "══════════════════════════════════════"
|
||
|
||
if [[ ${TESTS_FAILED} -gt 0 ]]; then
|
||
echo -e "${RED}测试失败${NC}"
|
||
exit 1
|
||
else
|
||
echo -e "${GREEN}全部测试通过${NC}"
|
||
exit 0
|
||
fi
|
||
}
|
||
|
||
# 捕获 EXIT 信号
|
||
trap 'teardown' EXIT
|
||
|
||
main "$@" |