test(personal): cover PDF and public URL capture

This commit is contained in:
2026-07-12 16:26:59 +08:00
parent c7756f86e2
commit 50e2b9e5ff
7 changed files with 421 additions and 72 deletions
+230 -64
View File
@@ -13,15 +13,27 @@ PHONE_SUFFIX=$(( (10#$(date +%s) + $$ + RANDOM) % 99999998 + 1 ))
PHONE_A="138$(printf '%08d' "$PHONE_SUFFIX")"
PHONE_B="138$(printf '%08d' $((PHONE_SUFFIX % 99999999 + 1)))"
TITLE="personal-assistant-smoke-$RUN_ID"
CONTENT="个人助理隔离烟测唯一标记 ${RUN_ID},物业催费沟通资料。"
TEXT_QUERY="Personal text isolation evidence"
CONTENT="个人助理隔离烟测唯一标记 ${RUN_ID},物业催费沟通资料。${TEXT_QUERY} for run ${RUN_ID}."
TEXT_TITLE="$TITLE-text"
PDF_TITLE="$TITLE-pdf"
URL_TITLE="$TITLE-url"
PDF_QUERY="Personal PDF verification evidence"
PUBLIC_URL="${AIHR_PERSONAL_SMOKE_PUBLIC_URL:-https://example.com/}"
PUBLIC_QUERY="${AIHR_PERSONAL_SMOKE_PUBLIC_QUERY:-Example Domain}"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/wygj-personal-smoke.XXXXXX")"
chmod 700 "$TMP_ROOT"
TOKEN_A="" CLIENT_A="" TOKEN_B="" CLIENT_B=""
USER_A="" USER_B="" ITEM_ID="" OSS_ID="" OBJECT_KEY="" OSS_URL="" JOB_ID="" SESSION_ID=""
USER_A="" USER_B="" SESSION_ID=""
OWN_PHONE_A=0 OWN_PHONE_B=0 CLEANED=0
HTTP_STATUS="" HTTP_BODY="" LOGIN_TOKEN="" LOGIN_CLIENT=""
TEMP_FILES=()
ITEM_IDS=()
OSS_IDS=()
OBJECT_KEYS=()
OSS_URLS=()
JOB_IDS=()
fail() { echo "FAIL: $*" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || fail "missing command: $1"; }
@@ -38,6 +50,38 @@ register_tmp() {
TEMP_FILES+=("$path")
}
append_item_metadata() {
local item_id="$1" item_title="$2" oss_id object_key oss_url
read -r oss_id object_key oss_url <<<"$(mysql "select o.oss_id,o.file_name,o.url from sys_oss o
join aihr_personal_item i on i.oss_id=o.oss_id
where i.id=$item_id and i.owner_user_id=$USER_A and i.title='$item_title' limit 1")"
[[ "$oss_id" =~ ^[0-9]+$ && "$object_key" =~ ^personal/000000/$USER_A/$item_id/[A-Za-z0-9._/-]+$ \
&& "$object_key" != *".."* && -n "$oss_url" ]] || fail "unexpected personal OSS ownership metadata item=$item_id"
ITEM_IDS+=("$item_id")
OSS_IDS+=("$oss_id")
OBJECT_KEYS+=("$object_key")
OSS_URLS+=("$oss_url")
}
discover_run_items() {
[[ "$USER_A" =~ ^[0-9]+$ ]] || return 0
while IFS=$'\t' read -r item_id oss_id object_key oss_url; do
[[ "$item_id" =~ ^[0-9]+$ ]] || continue
local seen=0 existing
for existing in "${ITEM_IDS[@]}"; do
[[ "$existing" == "$item_id" ]] && seen=1 && break
done
[[ "$seen" == 0 ]] || continue
ITEM_IDS+=("$item_id")
OSS_IDS+=("${oss_id:-}")
OBJECT_KEYS+=("${object_key:-}")
OSS_URLS+=("${oss_url:-}")
done < <(mysql "select i.id,coalesce(o.oss_id,''),coalesce(o.file_name,''),coalesce(o.url,'')
from aihr_personal_item i left join sys_oss o on o.oss_id=i.oss_id
where i.tenant_id='000000' and i.owner_user_id=$USER_A and i.title like '$TITLE-%'
order by i.id")
}
cleanup_once() {
[[ "$CLEANED" == 0 ]] || return 0
CLEANED=1
@@ -51,36 +95,30 @@ cleanup_once() {
if [[ "$OWN_PHONE_B" == 1 && ! "$USER_B" =~ ^[0-9]+$ ]]; then
USER_B="$(mysql "select user_id from sys_user where phonenumber='$PHONE_B' and remark='移动端短信自动注册' order by user_id desc limit 1" | head -1)"
fi
if [[ "$USER_A" =~ ^[0-9]+$ && ! "$ITEM_ID" =~ ^[0-9]+$ ]]; then
ITEM_ID="$(mysql "select id from aihr_personal_item where tenant_id='000000' and owner_user_id=$USER_A and title='$TITLE' order by id desc limit 1" | head -1)"
fi
if [[ "$USER_A" =~ ^[0-9]+$ && "$ITEM_ID" =~ ^[0-9]+$ ]]; then
if [[ ! "$OSS_ID" =~ ^[0-9]+$ || -z "$OBJECT_KEY" ]]; then
read -r OSS_ID OBJECT_KEY OSS_URL <<<"$(mysql "select o.oss_id,o.file_name,o.url from sys_oss o join aihr_personal_item i on i.oss_id=o.oss_id where i.id=$ITEM_ID and i.owner_user_id=$USER_A and i.title='$TITLE' limit 1")"
fi
[[ "$JOB_ID" =~ ^[0-9]+$ ]] || JOB_ID="$(mysql "select id from aihr_personal_cleanup_job where tenant_id='000000' and owner_user_id=$USER_A and item_id=$ITEM_ID order by id desc limit 1" | head -1)"
fi
discover_run_items
if [[ "$USER_A" =~ ^[0-9]+$ && ! "$SESSION_ID" =~ ^[0-9]+$ ]]; then
SESSION_ID="$(mysql "select s.id from aihr_personal_chat_session s join aihr_personal_chat_message m on m.session_id=s.id and m.owner_user_id=s.owner_user_id where s.tenant_id='000000' and s.owner_user_id=$USER_A and m.content like '%$RUN_ID%' order by s.id desc limit 1" | head -1)"
fi
if [[ "$USER_A" =~ ^[0-9]+$ && "$ITEM_ID" =~ ^[0-9]+$ ]]; then
local index item_id oss_id object_key
for index in "${!ITEM_IDS[@]}"; do
item_id="${ITEM_IDS[$index]}"
oss_id="${OSS_IDS[$index]:-}"
object_key="${OBJECT_KEYS[$index]:-}"
[[ "$item_id" =~ ^[0-9]+$ && "$USER_A" =~ ^[0-9]+$ ]] || continue
curl -sS -X POST "$QDRANT_URL/collections/$QDRANT_COLLECTION/points/delete?wait=true" \
-H 'Content-Type: application/json' \
--data "{\"filter\":{\"must\":[{\"key\":\"tenant_id\",\"match\":{\"value\":\"000000\"}},{\"key\":\"owner_user_id\",\"match\":{\"value\":$USER_A}},{\"key\":\"item_id\",\"match\":{\"value\":$ITEM_ID}}]}}" >/dev/null 2>&1 || true
fi
if [[ "$OSS_ID" =~ ^[0-9]+$ && "$USER_A" =~ ^[0-9]+$ && "$ITEM_ID" =~ ^[0-9]+$ \
&& "$OBJECT_KEY" =~ ^personal/000000/$USER_A/$ITEM_ID/[A-Za-z0-9._/-]+$ \
&& "$OBJECT_KEY" != *".."* ]]; then
docker run --rm --network "$DOCKER_NETWORK" \
-e 'MC_HOST_local=http://ruoyi:ruoyi123@wygj-minio:9000' minio/mc \
rm --force "local/ruoyi-personal/$OBJECT_KEY" >/dev/null 2>&1 || true
mysql "delete from sys_oss where oss_id=$OSS_ID and service='personal-minio' and file_name='$OBJECT_KEY'" >/dev/null 2>&1 || true
fi
if [[ "$ITEM_ID" =~ ^[0-9]+$ && "$USER_A" =~ ^[0-9]+$ ]]; then
mysql "delete from aihr_personal_fragment where tenant_id='000000' and owner_user_id=$USER_A and item_id=$ITEM_ID;
delete from aihr_personal_cleanup_job where tenant_id='000000' and owner_user_id=$USER_A and item_id=$ITEM_ID;
delete from aihr_personal_item where tenant_id='000000' and owner_user_id=$USER_A and id=$ITEM_ID and title='$TITLE';" >/dev/null 2>&1 || true
fi
--data "{\"filter\":{\"must\":[{\"key\":\"tenant_id\",\"match\":{\"value\":\"000000\"}},{\"key\":\"owner_user_id\",\"match\":{\"value\":$USER_A}},{\"key\":\"item_id\",\"match\":{\"value\":$item_id}}]}}" >/dev/null 2>&1 || true
if [[ "$oss_id" =~ ^[0-9]+$ && "$object_key" =~ ^personal/000000/$USER_A/$item_id/[A-Za-z0-9._/-]+$ \
&& "$object_key" != *".."* ]]; then
docker run --rm --network "$DOCKER_NETWORK" \
-e 'MC_HOST_local=http://ruoyi:ruoyi123@wygj-minio:9000' minio/mc \
rm --force "local/ruoyi-personal/$object_key" >/dev/null 2>&1 || true
mysql "delete from sys_oss where oss_id=$oss_id and service='personal-minio' and file_name='$object_key'" >/dev/null 2>&1 || true
fi
mysql "delete from aihr_personal_fragment where tenant_id='000000' and owner_user_id=$USER_A and item_id=$item_id;
delete from aihr_personal_cleanup_job where tenant_id='000000' and owner_user_id=$USER_A and item_id=$item_id;
delete from aihr_personal_item where tenant_id='000000' and owner_user_id=$USER_A and id=$item_id and title like '$TITLE-%';" >/dev/null 2>&1 || true
done
if [[ "$SESSION_ID" =~ ^[0-9]+$ && "$USER_A" =~ ^[0-9]+$ ]]; then
mysql "delete from aihr_personal_chat_message where tenant_id='000000' and owner_user_id=$USER_A and session_id=$SESSION_ID;
delete from aihr_personal_chat_session where tenant_id='000000' and owner_user_id=$USER_A and id=$SESSION_ID;" >/dev/null 2>&1 || true
@@ -140,6 +178,17 @@ request() {
HTTP_BODY="$(cat "$tmp")"
rm -f "$tmp"
}
request_file() {
local path="$1" token="$2" client="$3" file="$4" title="$5" tmp
tmp="$(mktemp "$TMP_ROOT/http-file.XXXXXX")"
register_tmp "$tmp"
HTTP_STATUS="$(curl -k -sS -X POST "$API_URL$path" -o "$tmp" -w '%{http_code}' \
-H "Authorization: Bearer $token" -H "clientid: $client" \
-F "file=@$file;type=application/pdf;filename=$(basename "$file")" \
-F "title=$title" || printf '000')"
HTTP_BODY="$(cat "$tmp")"
rm -f "$tmp"
}
expect_success() {
local label="$1"
expect_code "$HTTP_STATUS" 200 "$label HTTP"
@@ -162,7 +211,68 @@ login() {
LOGIN_CLIENT="$(jq -er '.data.client_id' <<<"$HTTP_BODY")"
}
need curl; need jq; need docker; need redis-cli
wait_ready() {
local item_id="$1" label="$2" status=""
for _ in {1..60}; do
request GET "/api/aihr/personal-assistant/items/$item_id" "$TOKEN_A" "$CLIENT_A"
expect_success "$label detail poll"
status="$(jq -r '.data.status // empty' <<<"$HTTP_BODY")"
[[ "$status" == READY ]] && return 0
[[ "$status" != FAILED ]] || fail "$label processing failed code=$(jq -r '.data.errorCode // empty' <<<"$HTTP_BODY")"
sleep 1
done
fail "$label did not become READY lastStatus=$status"
}
assert_anonymous_private() {
local url="$1" label="$2" tmp status
tmp="$(mktemp "$TMP_ROOT/anonymous.XXXXXX")"
register_tmp "$tmp"
status="$(curl -sS -o "$tmp" -w '%{http_code}' "$url" || printf '000')"
expect_code "$status" 403 "$label anonymous GET"
}
assert_search_hit() {
local item_id="$1" query="$2" expected_type="$3" label="$4" item_ids_json
item_ids_json="$(jq -cn --argjson item "$item_id" '[$item]')"
request POST /api/aihr/personal-assistant/search "$TOKEN_A" "$CLIENT_A" \
"$(jq -cn --arg query "$query" --argjson ids "$item_ids_json" \
'{queryText:$query,scope:["PERSONAL"],itemIds:$ids,limit:10}')"
expect_success "$label itemIds search"
[[ "$(jq --arg item "$item_id" --arg type "$expected_type" \
'[.data.hits[] | select((.itemId|tostring)==$item and .sourceType==$type)]|length' <<<"$HTTP_BODY")" -ge 1 ]] \
|| fail "$label search did not return item=$item_id type=$expected_type query=$query hits=$(jq -c '.data.hits // []' <<<"$HTTP_BODY")"
}
assert_b_isolation() {
local item_id="$1" label="$2" operation
for operation in detail download retry delete; do
case "$operation" in
detail) request GET "/api/aihr/personal-assistant/items/$item_id" "$TOKEN_B" "$CLIENT_B" ;;
download) request GET "/api/aihr/personal-assistant/items/$item_id/download-url" "$TOKEN_B" "$CLIENT_B" ;;
retry) request POST "/api/aihr/personal-assistant/items/$item_id/retry" "$TOKEN_B" "$CLIENT_B" ;;
delete) request DELETE "/api/aihr/personal-assistant/items/$item_id" "$TOKEN_B" "$CLIENT_B" ;;
esac
expect_error PERSONAL_ITEM_NOT_FOUND "B $operation A $label item"
done
request POST /api/aihr/personal-assistant/search "$TOKEN_B" "$CLIENT_B" \
"$(jq -cn --argjson item "$item_id" '{queryText:"smoke",scope:["PERSONAL"],itemIds:[$item],limit:10}')"
expect_error PERSONAL_ITEM_NOT_FOUND "B itemIds search A $label isolation"
}
need curl; need jq; need docker; need redis-cli; need cupsfilter
case "$PUBLIC_URL" in
http://localhost*|https://localhost*|http://127.*|https://127.*|http://\[*|https://\[*|http://169.254.*|https://169.254.*)
fail "AIHR_PERSONAL_SMOKE_PUBLIC_URL must be a public URL, not localhost/private metadata"
;;
http://*|https://*) ;;
*) fail "AIHR_PERSONAL_SMOKE_PUBLIC_URL must use http or https" ;;
esac
PUBLIC_EFFECTIVE_URL="$(curl --fail --location --silent --show-error --max-time 20 \
--output /dev/null --write-out '%{url_effective}' "$PUBLIC_URL")" \
|| fail "public URL unreachable: $PUBLIC_URL"
[[ "$PUBLIC_EFFECTIVE_URL" == http://* || "$PUBLIC_EFFECTIVE_URL" == https://* ]] \
|| fail "public URL did not resolve to HTTP(S): $PUBLIC_URL"
request GET /auth/tenant/list
expect_success "backend health"
docker ps --format '{{.Names}}' | grep -qx "$DB_CONTAINER" || fail "database container not running: $DB_CONTAINER"
@@ -184,46 +294,64 @@ USER_B="$(mysql "select user_id from sys_user where phonenumber='$PHONE_B' and r
[[ "$USER_B" =~ ^[0-9]+$ && "$USER_A" != "$USER_B" ]] || fail "B smoke user was not created independently"
request POST /api/aihr/personal-assistant/items/text "$TOKEN_A" "$CLIENT_A" \
"{\"title\":\"$TITLE\",\"content\":\"$CONTENT\",\"tags\":[\"smoke\",\"$RUN_ID\"]}"
"$(jq -cn --arg title "$TEXT_TITLE" --arg content "$CONTENT" --arg run "$RUN_ID" \
'{title:$title,content:$content,tags:["smoke",$run]}')"
expect_success "A create text item"
ITEM_ID="$(jq -er '.data.itemId | tostring' <<<"$HTTP_BODY")"
read -r OSS_ID OBJECT_KEY OSS_URL <<<"$(mysql "select o.oss_id,o.file_name,o.url from sys_oss o join aihr_personal_item i on i.oss_id=o.oss_id where i.id=$ITEM_ID and i.owner_user_id=$USER_A and i.title='$TITLE'")"
[[ "$OSS_ID" =~ ^[0-9]+$ && "$OBJECT_KEY" =~ ^personal/000000/$USER_A/$ITEM_ID/[A-Za-z0-9._/-]+$ ]] || fail "unexpected personal OSS ownership metadata"
TEXT_ITEM_ID="$(jq -er '.data.itemId | tostring' <<<"$HTTP_BODY")"
append_item_metadata "$TEXT_ITEM_ID" "$TEXT_TITLE"
for _ in {1..40}; do
request GET "/api/aihr/personal-assistant/items/$ITEM_ID" "$TOKEN_A" "$CLIENT_A"
[[ "$(jq -r '.data.status // empty' <<<"$HTTP_BODY")" == READY ]] && break
sleep 1
PDF_TEXT="$TMP_ROOT/personal-smoke-$RUN_ID.txt"
PDF_FILE="$TMP_ROOT/personal-smoke-$RUN_ID.pdf"
printf 'Personal PDF verification evidence for run %s. Property service fee communication checklist.\n' \
"$RUN_ID" >"$PDF_TEXT"
cupsfilter -m application/pdf "$PDF_TEXT" >"$PDF_FILE" 2>"$TMP_ROOT/cupsfilter.log" \
|| fail "cupsfilter failed to generate smoke PDF"
[[ -s "$PDF_FILE" && "$(head -c 4 "$PDF_FILE")" == '%PDF' ]] || fail "cupsfilter output is not a PDF"
request_file /api/aihr/personal-assistant/items/file "$TOKEN_A" "$CLIENT_A" "$PDF_FILE" "$PDF_TITLE"
expect_success "A create PDF item"
PDF_ITEM_ID="$(jq -er '.data.itemId | tostring' <<<"$HTTP_BODY")"
append_item_metadata "$PDF_ITEM_ID" "$PDF_TITLE"
request POST /api/aihr/personal-assistant/items/url "$TOKEN_A" "$CLIENT_A" \
"$(jq -cn --arg url "$PUBLIC_URL" --arg title "$URL_TITLE" '{url:$url,title:$title}')"
expect_success "A create public URL item"
URL_ITEM_ID="$(jq -er '.data.itemId | tostring' <<<"$HTTP_BODY")"
append_item_metadata "$URL_ITEM_ID" "$URL_TITLE"
expect_code "${#ITEM_IDS[@]}" 3 "three personal items captured"
wait_ready "$TEXT_ITEM_ID" "A text item"
expect_code "$(jq -r '.data.sourceType' <<<"$HTTP_BODY")" TEXT "A text source type"
wait_ready "$PDF_ITEM_ID" "A PDF item"
expect_code "$(jq -r '.data.sourceType' <<<"$HTTP_BODY")" FILE "A PDF source type"
expect_code "$(jq -r '.data.mimeType' <<<"$HTTP_BODY")" application/pdf "A PDF mime type"
wait_ready "$URL_ITEM_ID" "A public URL item"
expect_code "$(jq -r '.data.sourceType' <<<"$HTTP_BODY")" URL "A URL source type"
expect_code "$(jq -r '.data.originalUrl' <<<"$HTTP_BODY")" "$PUBLIC_EFFECTIVE_URL" "A URL originalUrl"
for index in "${!OSS_URLS[@]}"; do
assert_anonymous_private "${OSS_URLS[$index]}" "personal object ${ITEM_IDS[$index]}"
done
expect_success "A item detail"
expect_code "$(jq -r '.data.status' <<<"$HTTP_BODY")" READY "A text item processing"
anon_tmp="$(mktemp "$TMP_ROOT/anonymous.XXXXXX")"; register_tmp "$anon_tmp"
anon_status="$(curl -sS -o "$anon_tmp" -w '%{http_code}' "$OSS_URL" || printf '000')"
expect_code "$anon_status" 403 "personal object anonymous GET"
request POST /api/aihr/personal-assistant/search "$TOKEN_A" "$CLIENT_A" \
"{\"queryText\":\"隔离烟测\",\"scope\":[\"PERSONAL\"],\"itemIds\":[\"$ITEM_ID\"],\"limit\":10}"
expect_success "A itemIds search"
[[ "$(jq --arg item "$ITEM_ID" '[.data.hits[] | select((.itemId|tostring)==$item)]|length' <<<"$HTTP_BODY")" -ge 1 ]] || fail "A search did not return own item"
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" \
"{\"queryText\":\"隔离烟测资料是什么 $RUN_ID\",\"scope\":[\"PERSONAL\"],\"itemIds\":[\"$ITEM_ID\"]}"
"$(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"
request GET "/api/aihr/personal-assistant/sessions/$SESSION_ID" "$TOKEN_B" "$CLIENT_B"
expect_error PERSONAL_SESSION_NOT_FOUND "B session isolation"
for operation in detail download retry delete; do
case "$operation" in
detail) request GET "/api/aihr/personal-assistant/items/$ITEM_ID" "$TOKEN_B" "$CLIENT_B" ;;
download) request GET "/api/aihr/personal-assistant/items/$ITEM_ID/download-url" "$TOKEN_B" "$CLIENT_B" ;;
retry) request POST "/api/aihr/personal-assistant/items/$ITEM_ID/retry" "$TOKEN_B" "$CLIENT_B" ;;
delete) request DELETE "/api/aihr/personal-assistant/items/$ITEM_ID" "$TOKEN_B" "$CLIENT_B" ;;
esac
expect_error PERSONAL_ITEM_NOT_FOUND "B $operation A item"
done
request POST /api/aihr/personal-assistant/search "$TOKEN_B" "$CLIENT_B" \
"{\"queryText\":\"隔离烟测\",\"scope\":[\"PERSONAL\"],\"itemIds\":[\"$ITEM_ID\"],\"limit\":10}"
expect_error PERSONAL_ITEM_NOT_FOUND "B itemIds search isolation"
assert_b_isolation "$TEXT_ITEM_ID" text
assert_b_isolation "$PDF_ITEM_ID" PDF
assert_b_isolation "$URL_ITEM_ID" URL
for blocked_url in 'http://127.0.0.1/admin' 'http://169.254.169.254/latest/meta-data/'; do
request POST /api/aihr/personal-assistant/items/url "$TOKEN_A" "$CLIENT_A" \
@@ -231,9 +359,47 @@ for blocked_url in 'http://127.0.0.1/admin' 'http://169.254.169.254/latest/meta-
expect_error PERSONAL_URL_BLOCKED "SSRF $blocked_url"
done
request DELETE "/api/aihr/personal-assistant/items/$ITEM_ID" "$TOKEN_A" "$CLIENT_A"; expect_success "A delete"
JOB_ID="$(jq -er '.data.cleanupJobId | tostring' <<<"$HTTP_BODY")"
request DELETE "/api/aihr/personal-assistant/items/$ITEM_ID" "$TOKEN_A" "$CLIENT_A"; expect_success "A repeat delete"
expect_code "$(jq -r '.data.cleanupJobId | tostring' <<<"$HTTP_BODY")" "$JOB_ID" "idempotent delete job"
request GET "/api/aihr/personal-assistant/items/$ITEM_ID" "$TOKEN_A" "$CLIENT_A"; expect_error PERSONAL_ITEM_NOT_FOUND "A item hidden immediately"
echo "PASS: personal assistant isolation, SSRF, readiness and deletion gates run=$RUN_ID"
for item_id in "$TEXT_ITEM_ID" "$PDF_ITEM_ID" "$URL_ITEM_ID"; do
request DELETE "/api/aihr/personal-assistant/items/$item_id" "$TOKEN_A" "$CLIENT_A"
expect_success "A delete item=$item_id"
JOB_IDS+=("$(jq -er '.data.cleanupJobId | tostring' <<<"$HTTP_BODY")")
request GET "/api/aihr/personal-assistant/items/$item_id" "$TOKEN_A" "$CLIENT_A"
expect_error PERSONAL_ITEM_NOT_FOUND "A item=$item_id hidden immediately"
done
request DELETE "/api/aihr/personal-assistant/items/$TEXT_ITEM_ID" "$TOKEN_A" "$CLIENT_A"
expect_success "A repeat text delete"
expect_code "$(jq -r '.data.cleanupJobId | tostring' <<<"$HTTP_BODY")" "${JOB_IDS[0]}" "idempotent delete job"
for _ in {1..75}; do
cleanup_done="$(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)
and status='DONE'")"
[[ "$cleanup_done" == 3 ]] && break
sleep 1
done
expect_code "$cleanup_done" 3 "three cleanup jobs completed"
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"