#!/usr/bin/env bash set -euo pipefail source "$(dirname "$0")/lib.sh" bundle_hash=547cec172aab2f4d418d73d4f5fc0d2794e971b7 bundle_torrent=ZDQ6aW5mb2Q1OmZpbGVzbGQ2Omxlbmd0aGkxNjM4NGU0OnBhdGhsOTphbHBoYS5iaW5lZWQ2Omxlbmd0aGkxNjM4NGU0OnBhdGhsNjpuZXN0ZWQ5OmJyYXZvLmJpbmVlZDY6bGVuZ3RoaTE2Mzg0ZTQ6cGF0aGw2Om5lc3RlZDExOmNoYXJsaWUuYmluZWVkNjpsZW5ndGhpMTYzODRlNDpwYXRobDk6ZGVsdGEuYmluZWVlNDpuYW1lNjpidW5kbGUxMjpwaWVjZSBsZW5ndGhpMTYzODRlNjpwaWVjZXM4MDp9tnus7uyzpCC/N6e+ykpFGF+PPHmZb79ZCq2dp84v4EgIIzMZE7bwEMQCvl+7qybWEDECinYwiKcGKn82sWFjoNungr001vTmtGeLhKBGd2Vl legacy_bundle_hash=305af159425d76140eea48d20cfcfbb3f8dccf46 shared_hash=03fbcf663bd3fe80c298ca54a55780a6b1d4b43b shared_torrent=ZDQ6aW5mb2Q1OmZpbGVzbGQ2Omxlbmd0aGkxNjM4NGU0OnBhdGhsNjpuZXN0ZWQ5OmJyYXZvLmJpbmVlZTQ6bmFtZTY6YnVuZGxlMTI6cGllY2UgbGVuZ3RoaTE2Mzg0ZTY6cGllY2VzMjA6eZlvv1kKrZ2nzi/gSAgjMxkTtvBlZQ== cancel_hash=330be0cb7c2201135a2de63b28e77993745ff688 cancel_torrent=ZDQ6aW5mb2Q2Omxlbmd0aGkzMWU0Om5hbWUxMTpmaXh0dXJlLmJpbjEyOnBpZWNlIGxlbmd0aGkxNjM4NGU2OnBpZWNlczIwOlFlB05MtIUU4oem2MNz5LJW/GI6ZWU= control_scenario() { compose_control exec -T control \ python /e2e/scenarios/complex_jobs.py "$@" } qb_endpoint() { case "$1" in cache-1) printf '%s' http://qb-cache-1:8080/api/v2 ;; cache-2) printf '%s' http://qb-cache-2:8080/api/v2 ;; archive-1) printf '%s' http://qb-archive-1:8080/api/v2 ;; archive-2) printf '%s' http://qb-archive-2:8080/api/v2 ;; *) printf 'unknown E2E node: %s\n' "$1" >&2; return 2 ;; esac } wait_qb_present() { local node=$1 local info_hash=$2 local deadline=$((SECONDS + 30)) while (( SECONDS < deadline )); do local records records=$(compose_node "$node" exec -T qbittorrent curl -fsS \ "http://127.0.0.1:8080/api/v2/torrents/info?hashes=$info_hash") if [[ "$records" != "[]" ]]; then return fi sleep 1 done printf 'timed out waiting for torrent %s on %s\n' \ "$info_hash" "$node" >&2 return 1 } delete_torrent() { local node=$1 local info_hash=$2 compose_node "$node" exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/delete \ --data-urlencode "hashes=$info_hash" \ --data-urlencode "deleteFiles=true" >/dev/null || true control_scenario assert-qb \ --endpoint "$(qb_endpoint "$node")" \ --info-hash "$info_hash" \ --absent >/dev/null } install_torrent() { local node=$1 local info_hash=$2 local encoded=$3 local save_path=$4 local selected=$5 local all_indices=$6 compose_node "$node" exec -T qbittorrent /bin/sh -c \ "printf '%s' '$encoded' | base64 -d > /tmp/e2e.torrent" compose_node "$node" exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/add \ -F torrents=@/tmp/e2e.torrent \ -F "savepath=$save_path" \ -F stopped=true >/dev/null wait_qb_present "$node" "$info_hash" compose_node "$node" exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/filePrio \ --data-urlencode "hash=$info_hash" \ --data-urlencode "id=$all_indices" \ --data-urlencode "priority=0" >/dev/null compose_node "$node" exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/filePrio \ --data-urlencode "hash=$info_hash" \ --data-urlencode "id=$selected" \ --data-urlencode "priority=1" >/dev/null compose_node "$node" exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/recheck \ --data-urlencode "hashes=$info_hash" >/dev/null } assert_qb() { local node=$1 local info_hash=$2 local selected=$3 control_scenario assert-qb \ --endpoint "$(qb_endpoint "$node")" \ --info-hash "$info_hash" \ --selected "$selected" >/dev/null } for node in cache-1 cache-2 archive-1 archive-2; do delete_torrent "$node" "$legacy_bundle_hash" delete_torrent "$node" "$bundle_hash" delete_torrent "$node" "$shared_hash" delete_torrent "$node" "$cancel_hash" compose_node "$node" exec -T qbittorrent /bin/sh -c \ 'rm -f /downloads/bundle/alpha.bin \ /downloads/bundle/nested/bravo.bin \ /downloads/bundle/nested/charlie.bin \ /downloads/bundle/delta.bin \ /downloads/bundle/unknown.keep \ /downloads/fixture.bin; \ rmdir /downloads/bundle/nested /downloads/bundle 2>/dev/null || true' compose_node "$node" exec -T qbittorrent /bin/sh -c \ 'chown -R 1001:1001 /downloads/bundle 2>/dev/null || true' done compose_node cache-1 exec -T --user 1001:1001 qbittorrent /bin/sh -c \ 'mkdir -p /downloads/bundle/nested; \ dd if=/dev/zero bs=16384 count=1 2>/dev/null | tr "\000" A \ > /downloads/bundle/alpha.bin; \ dd if=/dev/zero bs=16384 count=1 2>/dev/null | tr "\000" C \ > /downloads/bundle/nested/charlie.bin' install_torrent cache-1 "$bundle_hash" "$bundle_torrent" \ /downloads '0|2' '0|1|2|3' assert_qb cache-1 "$bundle_hash" '0,2' compose_node cache-2 exec -T --user 1001:1001 qbittorrent /bin/sh -c \ 'mkdir -p /downloads/bundle/nested; \ dd if=/dev/zero bs=16384 count=1 2>/dev/null | tr "\000" B \ > /downloads/bundle/nested/bravo.bin; \ dd if=/dev/zero bs=16384 count=1 2>/dev/null | tr "\000" D \ > /downloads/bundle/delta.bin' install_torrent cache-2 "$bundle_hash" "$bundle_torrent" \ /downloads '1|3' '0|1|2|3' assert_qb cache-2 "$bundle_hash" '1,3' control_scenario stale-preview \ --operation archive \ --source-client cache-1 \ --target-client archive-1 \ --info-hash "$bundle_hash" \ --selected '0' >/dev/null control_scenario transfer \ --operation archive \ --source-client cache-1 \ --target-client archive-1 \ --info-hash "$bundle_hash" \ --selected '0,2' \ --expected-selection '0,2' \ --expected-delta '0,2' assert_qb archive-1 "$bundle_hash" '0,2' control_scenario preview-error \ --operation evict_cache \ --cache-client cache-2 \ --info-hash "$bundle_hash" \ --status 400 \ --contains 'missing file indices' control_scenario transfer \ --operation archive \ --source-client cache-2 \ --target-client archive-2 \ --info-hash "$bundle_hash" \ --selected '1,3' \ --expected-selection '1,3' \ --expected-delta '1,3' assert_qb archive-2 "$bundle_hash" '1,3' compose_node cache-2 exec -T --user 1001:1001 qbittorrent /bin/sh -c \ 'dd if=/dev/zero bs=16384 count=1 2>/dev/null | tr "\000" A \ > /downloads/bundle/alpha.bin; \ dd if=/dev/zero bs=16384 count=1 2>/dev/null | tr "\000" C \ > /downloads/bundle/nested/charlie.bin' compose_node cache-2 exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/filePrio \ --data-urlencode "hash=$bundle_hash" \ --data-urlencode 'id=0|1|2|3' \ --data-urlencode priority=1 >/dev/null compose_node cache-2 exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/recheck \ --data-urlencode "hashes=$bundle_hash" >/dev/null assert_qb cache-2 "$bundle_hash" '0,1,2,3' control_scenario evict \ --cache-client cache-2 \ --info-hash "$bundle_hash" \ --minimum-coverage-proofs 2 control_scenario assert-qb \ --endpoint "$(qb_endpoint cache-2)" \ --info-hash "$bundle_hash" \ --absent >/dev/null control_scenario transfer \ --operation unarchive \ --source-client archive-1 \ --target-client cache-2 \ --info-hash "$bundle_hash" \ --selected '0,2' \ --expected-selection '0,2' \ --expected-delta '0,2' assert_qb cache-2 "$bundle_hash" '0,2' control_scenario transfer \ --operation unarchive \ --source-client archive-2 \ --target-client cache-2 \ --info-hash "$bundle_hash" \ --selected '1,3' \ --expected-selection '1,3' \ --expected-delta '1,3' assert_qb cache-2 "$bundle_hash" '0,1,2,3' control_scenario transfer \ --operation archive \ --source-client cache-2 \ --target-client archive-1 \ --info-hash "$bundle_hash" \ --selected '1,3' \ --expected-selection '1,3' \ --expected-delta '1,3' assert_qb archive-1 "$bundle_hash" '0,1,2,3' control_scenario transfer \ --operation archive \ --source-client cache-2 \ --target-client archive-2 \ --info-hash "$bundle_hash" \ --selected '0,2' \ --expected-selection '0,2' \ --expected-delta '0,2' assert_qb archive-2 "$bundle_hash" '0,1,2,3' control_scenario preview-error \ --operation archive \ --source-client cache-2 \ --target-client archive-1 \ --info-hash "$bundle_hash" \ --selected '0' \ --status 400 \ --contains 'already contains' control_scenario evict \ --cache-client cache-1 \ --info-hash "$bundle_hash" control_scenario transfer \ --operation unarchive \ --source-client archive-1 \ --target-client cache-1 \ --info-hash "$bundle_hash" \ --selected '1' \ --expected-selection '1' \ --expected-delta '1' assert_qb cache-1 "$bundle_hash" '1' compose_node cache-1 exec -T --user 1001:1001 qbittorrent /bin/sh -c \ 'printf "preserve-unknown\n" > /downloads/bundle/unknown.keep' install_torrent cache-1 "$shared_hash" "$shared_torrent" \ /downloads '0' '0' assert_qb cache-1 "$shared_hash" '0' control_scenario evict \ --cache-client cache-1 \ --info-hash "$bundle_hash" control_scenario assert-qb \ --endpoint "$(qb_endpoint cache-1)" \ --info-hash "$bundle_hash" \ --absent >/dev/null assert_qb cache-1 "$shared_hash" '0' compose_node cache-1 exec -T qbittorrent test -f \ /downloads/bundle/nested/bravo.bin compose_node cache-1 exec -T qbittorrent test -f \ /downloads/bundle/unknown.keep compose_node cache-1 exec -T --user 1001:1001 qbittorrent /bin/sh -c \ 'printf "archive-control-e2e-happy-path\n" > /downloads/fixture.bin' install_torrent cache-1 "$cancel_hash" "$cancel_torrent" \ /downloads '0' '0' assert_qb cache-1 "$cancel_hash" '0' control_scenario queued-cancel \ --operation archive \ --source-client cache-1 \ --target-client archive-1 \ --info-hash "$cancel_hash" compose_node cache-1 exec -T qbittorrent test -f /downloads/fixture.bin cancel_job=$(control_scenario drive \ --operation archive \ --source-client cache-1 \ --target-client archive-2 \ --info-hash "$cancel_hash" \ --through target_materialize) control_scenario cancel --job-id "$cancel_job" control_scenario assert-qb \ --endpoint "$(qb_endpoint archive-2)" \ --info-hash "$cancel_hash" \ --absent >/dev/null if compose_node archive-2 exec -T qbittorrent \ test -e /downloads/fixture.bin; then printf 'precommit cancellation left a materialized target file\n' >&2 exit 1 fi assert_qb cache-1 "$cancel_hash" '0' restart_job=$(control_scenario drive \ --operation archive \ --source-client cache-1 \ --target-client archive-1 \ --info-hash "$cancel_hash" \ --through source_stage) compose_control restart control sleep 2 control_scenario resume --job-id "$restart_job" assert_qb archive-1 "$cancel_hash" '0' delete_torrent archive-2 "$cancel_hash" replay_job=$(control_scenario drive \ --operation archive \ --source-client cache-1 \ --target-client archive-2 \ --info-hash "$cancel_hash" \ --through source_stage) compose_node archive-2 stop client sleep 1 control_scenario advance \ --job-id "$replay_job" \ --action syncthing_transfer >/dev/null compose_node archive-2 start client control_scenario resume --job-id "$replay_job" control_scenario assert-qb \ --endpoint "$(qb_endpoint archive-2)" \ --info-hash "$cancel_hash" \ --selected '0' \ --no-downloaded >/dev/null delete_torrent archive-2 "$cancel_hash" stale_source_job=$(control_scenario create-paused \ --operation archive \ --source-client cache-1 \ --target-client archive-2 \ --info-hash "$cancel_hash") compose_node cache-1 exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/filePrio \ --data-urlencode "hash=$cancel_hash" \ --data-urlencode id=0 \ --data-urlencode priority=0 >/dev/null control_scenario expect-failure \ --job-id "$stale_source_job" \ --contains 'source resource changed after job confirmation' compose_node cache-1 exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/filePrio \ --data-urlencode "hash=$cancel_hash" \ --data-urlencode id=0 \ --data-urlencode priority=1 >/dev/null compose_node cache-1 exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/recheck \ --data-urlencode "hashes=$cancel_hash" >/dev/null assert_qb cache-1 "$cancel_hash" '0' target_appeared_job=$(control_scenario create-paused \ --operation archive \ --source-client cache-1 \ --target-client archive-2 \ --info-hash "$cancel_hash") compose_node archive-2 exec -T qbittorrent /bin/sh -c \ "printf '%s' '$cancel_torrent' | base64 -d > /tmp/appeared.torrent" compose_node archive-2 exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/add \ -F torrents=@/tmp/appeared.torrent \ -F savepath=/downloads \ -F stopped=true >/dev/null wait_qb_present archive-2 "$cancel_hash" control_scenario expect-failure \ --job-id "$target_appeared_job" \ --contains 'target resource appeared after job confirmation' assert_qb cache-1 "$cancel_hash" '0' delete_torrent archive-2 "$cancel_hash" printf '%s\n' \ 'complex matrix passed: selective merges, union eviction, hostile retention,' \ 'queued/partial cancellation, restart/replay, and hostile state changes'