fix(personal): verify per-source citations and cleanup
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user