fix(personal): verify per-source citations and cleanup

This commit is contained in:
2026-07-12 16:35:06 +08:00
parent 50e2b9e5ff
commit fed32939e6
+65 -30
View File
@@ -244,6 +244,67 @@ assert_search_hit() {
|| fail "$label search did not return item=$item_id type=$expected_type query=$query hits=$(jq -c '.data.hits // []' <<<"$HTTP_BODY")"
}
assert_single_item_ask() {
local item_id="$1" query="$2" expected_type="$3" label="$4" session_json=null response_session
[[ "$SESSION_ID" =~ ^[0-9]+$ ]] && session_json="$SESSION_ID"
request POST /api/aihr/personal-assistant/ask "$TOKEN_A" "$CLIENT_A" \
"$(jq -cn --arg query "$query" --argjson item "$item_id" --argjson session "$session_json" \
'{sessionId:$session,queryText:$query,scope:["PERSONAL"],itemIds:[$item]}')"
expect_success "A $label single-item answer"
response_session="$(jq -er '.data.sessionId | tostring' <<<"$HTTP_BODY")"
[[ "$response_session" =~ ^[0-9]+$ ]] || fail "A $label answer did not persist session"
if [[ "$SESSION_ID" =~ ^[0-9]+$ ]]; then
expect_code "$response_session" "$SESSION_ID" "A $label answer session continuity"
else
SESSION_ID="$response_session"
fi
[[ "$(jq --arg item "$item_id" --arg type "$expected_type" \
'[.data.citations[] | select(.domain=="PERSONAL" and (.itemId|tostring)==$item and .sourceType==$type)]|length' \
<<<"$HTTP_BODY")" -ge 1 ]] \
|| fail "A $label answer lacks exact PERSONAL/$expected_type citation item=$item_id citations=$(jq -c '.data.citations // []' <<<"$HTTP_BODY")"
expect_code "$(jq --arg item "$item_id" --arg type "$expected_type" \
'[.data.citations[] | select(.domain!="PERSONAL" or (.itemId|tostring)!=$item or .sourceType!=$type)]|length' \
<<<"$HTTP_BODY")" 0 "A $label answer foreign citation count"
}
qdrant_item_count() {
local item_id="$1" collection_exists count=0
collection_exists="$(curl -sS "$QDRANT_URL/collections" \
| jq -er --arg name "$QDRANT_COLLECTION" '[.result.collections[] | select(.name==$name)]|length')" \
|| fail "Qdrant collection discovery failed"
if [[ "$collection_exists" -ge 1 ]]; then
count="$(curl -sS -X POST "$QDRANT_URL/collections/$QDRANT_COLLECTION/points/count" \
-H 'Content-Type: application/json' \
--data "{\"exact\":true,\"filter\":{\"must\":[{\"key\":\"tenant_id\",\"match\":{\"value\":\"000000\"}},{\"key\":\"owner_user_id\",\"match\":{\"value\":$USER_A}},{\"key\":\"item_id\",\"match\":{\"value\":$item_id}}]}}" \
| jq -er '.result.count')" || fail "Qdrant residual check failed item=$item_id"
fi
printf '%s' "$count"
}
assert_business_cleanup() {
local ids="$TEXT_ITEM_ID,$PDF_ITEM_ID,$URL_ITEM_ID" index
expect_code "$(mysql "select count(*) from aihr_personal_cleanup_job
where tenant_id='000000' and owner_user_id=$USER_A and item_id in ($ids) and status='DONE'")" \
3 "application cleanup jobs DONE"
expect_code "$(mysql "select count(*) from aihr_personal_item
where tenant_id='000000' and owner_user_id=$USER_A and id in ($ids) and status='DELETED'")" \
3 "application item tombstones DELETED"
expect_code "$(mysql "select count(*) from aihr_personal_fragment
where tenant_id='000000' and owner_user_id=$USER_A and item_id in ($ids)")" \
0 "application fragment residual"
for index in "${!OSS_IDS[@]}"; do
expect_code "$(mysql "select count(*) from sys_oss where oss_id=${OSS_IDS[$index]}")" \
0 "application OSS row ${OSS_IDS[$index]} residual"
if docker run --rm --network "$DOCKER_NETWORK" \
-e 'MC_HOST_local=http://ruoyi:ruoyi123@wygj-minio:9000' minio/mc \
stat "local/ruoyi-personal/${OBJECT_KEYS[$index]}" >/dev/null 2>&1; then
fail "application MinIO object ${OBJECT_KEYS[$index]} residual"
fi
expect_code "$(qdrant_item_count "${ITEM_IDS[$index]}")" 0 \
"application Qdrant points item=${ITEM_IDS[$index]} residual"
done
}
assert_b_isolation() {
local item_id="$1" label="$2" operation
for operation in detail download retry delete; do
@@ -335,17 +396,9 @@ assert_search_hit "$TEXT_ITEM_ID" "$TEXT_QUERY" TEXT text
assert_search_hit "$PDF_ITEM_ID" "$PDF_QUERY" FILE PDF
assert_search_hit "$URL_ITEM_ID" "$PUBLIC_QUERY" URL URL
ALL_ITEM_IDS_JSON="$(printf '%s\n' "$TEXT_ITEM_ID" "$PDF_ITEM_ID" "$URL_ITEM_ID" | jq -s 'map(tonumber)')"
request POST /api/aihr/personal-assistant/ask "$TOKEN_A" "$CLIENT_A" \
"$(jq -cn --arg query "$TEXT_QUERY $PDF_QUERY $PUBLIC_QUERY $RUN_ID" --argjson ids "$ALL_ITEM_IDS_JSON" \
'{queryText:$query,scope:["PERSONAL"],itemIds:$ids}')"
expect_success "A personal answer"
SESSION_ID="$(jq -er '.data.sessionId | tostring' <<<"$HTTP_BODY")"
[[ "$SESSION_ID" =~ ^[0-9]+$ ]] || fail "A answer did not persist session"
[[ "$(jq '[.data.citations[] | select(.sourceType=="TEXT")]|length' <<<"$HTTP_BODY")" -ge 1 ]] \
|| fail "A answer citations did not include the selected TEXT item"
[[ "$(jq '[.data.citations[] | select(.sourceType=="FILE" or .sourceType=="URL")]|length' <<<"$HTTP_BODY")" -ge 1 ]] \
|| fail "A answer citations did not include the selected PDF/URL item"
assert_single_item_ask "$TEXT_ITEM_ID" "$TEXT_QUERY" TEXT text
assert_single_item_ask "$PDF_ITEM_ID" "$PDF_QUERY" FILE PDF
assert_single_item_ask "$URL_ITEM_ID" "$PUBLIC_QUERY" URL URL
request GET "/api/aihr/personal-assistant/sessions/$SESSION_ID" "$TOKEN_B" "$CLIENT_B"
expect_error PERSONAL_SESSION_NOT_FOUND "B session isolation"
@@ -379,27 +432,9 @@ for _ in {1..75}; do
done
expect_code "$cleanup_done" 3 "three cleanup jobs completed"
assert_business_cleanup
cleanup_once
expect_code "$(mysql "select count(*) from aihr_personal_item where tenant_id='000000' and owner_user_id=$USER_A and title like '$TITLE-%'")" 0 "run items residual"
expect_code "$(mysql "select count(*) from aihr_personal_fragment where tenant_id='000000' and owner_user_id=$USER_A and item_id in ($TEXT_ITEM_ID,$PDF_ITEM_ID,$URL_ITEM_ID)")" 0 "run fragments residual"
expect_code "$(mysql "select count(*) from aihr_personal_cleanup_job where tenant_id='000000' and owner_user_id=$USER_A and item_id in ($TEXT_ITEM_ID,$PDF_ITEM_ID,$URL_ITEM_ID)")" 0 "run cleanup jobs residual"
qdrant_collection_exists="$(curl -sS "$QDRANT_URL/collections" \
| jq -er --arg name "$QDRANT_COLLECTION" '[.result.collections[] | select(.name==$name)]|length')" \
|| fail "Qdrant collection discovery failed"
for index in "${!OSS_IDS[@]}"; do
expect_code "$(mysql "select count(*) from sys_oss where oss_id=${OSS_IDS[$index]}")" 0 "run OSS row ${OSS_IDS[$index]} residual"
if docker run --rm --network "$DOCKER_NETWORK" \
-e 'MC_HOST_local=http://ruoyi:ruoyi123@wygj-minio:9000' minio/mc \
stat "local/ruoyi-personal/${OBJECT_KEYS[$index]}" >/dev/null 2>&1; then
fail "run MinIO object ${OBJECT_KEYS[$index]} residual"
fi
qdrant_count=0
if [[ "$qdrant_collection_exists" -ge 1 ]]; then
qdrant_count="$(curl -sS -X POST "$QDRANT_URL/collections/$QDRANT_COLLECTION/points/count" \
-H 'Content-Type: application/json' \
--data "{\"exact\":true,\"filter\":{\"must\":[{\"key\":\"tenant_id\",\"match\":{\"value\":\"000000\"}},{\"key\":\"owner_user_id\",\"match\":{\"value\":$USER_A}},{\"key\":\"item_id\",\"match\":{\"value\":${ITEM_IDS[$index]}}}]}}" \
| jq -er '.result.count')" || fail "Qdrant residual check failed item=${ITEM_IDS[$index]}"
fi
expect_code "$qdrant_count" 0 "run Qdrant points item=${ITEM_IDS[$index]} residual"
done
echo "PASS: personal assistant TEXT/PDF/public URL isolation, retrieval, privacy and cleanup gates run=$RUN_ID"