#!/usr/bin/env bash set -euo pipefail source "$(dirname "$0")/lib.sh" v2_hash=3a22a96fa77fb2617b98ea276c8d720d2b82b97a1a463e843d38c9def4f814fa v2_qb_prefix=${v2_hash:0:40} v2_torrent=ZDQ6aW5mb2Q5OmZpbGUgdHJlZWQ2OnYyLmJpbmQwOmQ2Omxlbmd0aGkxOGUxMTpwaWVjZXMgcm9vdDMyOryYO9v2kD7tZAErfryevFyLb0HWrdRyIk3CnxEgVd56ZWVlMTI6bWV0YSB2ZXJzaW9uaTJlNDpuYW1lNjp2Mi5iaW4xMjpwaWVjZSBsZW5ndGhpMTYzODRlZWU= hybrid_v1=41ea01236373d80fc64f390a73ef4924ac8d2904 hybrid_v2=d341089faab562d2c722821ec55a92b5a71578a94f2fa4cd5f849e640dc02b42 hybrid_qb_prefix=${hybrid_v2:0:40} hybrid_torrent=ZDQ6aW5mb2Q5OmZpbGUgdHJlZWQxMDpoeWJyaWQuYmluZDA6ZDY6bGVuZ3RoaTIyZTExOnBpZWNlcyByb290MzI6cQSAen8RmDzTs/9c24a6rb/7z0N8MjVuzXUvDbHywddlZWU2Omxlbmd0aGkyMmUxMjptZXRhIHZlcnNpb25pMmU0Om5hbWUxMDpoeWJyaWQuYmluMTI6cGllY2UgbGVuZ3RoaTE2Mzg0ZTY6cGllY2VzMjA6xQCrcqTOQ+LNUqZJC7p47gKZ/GhlZQ== stall_hash=330be0cb7c2201135a2de63b28e77993745ff688 stall_torrent=ZDQ6aW5mb2Q2Omxlbmd0aGkzMWU0Om5hbWUxMTpmaXh0dXJlLmJpbjEyOnBpZWNlIGxlbmd0aGkxNjM4NGU2OnBpZWNlczIwOlFlB05MtIUU4oem2MNz5LJW/GI6ZWU= control_complex() { compose_control exec -T control \ python /e2e/scenarios/complex_jobs.py "$@" } control_adversarial() { compose_control exec -T control \ python /e2e/scenarios/adversarial_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 } delete_hash() { 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 } install_torrent() { local node=$1 local name=$2 local encoded=$3 compose_node "$node" exec -T qbittorrent /bin/sh -c \ "printf '%s' '$encoded' | base64 -d > /tmp/phase8.torrent" \ || return local attempt for attempt in 1 2 3; do if compose_node "$node" exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/add \ -F torrents=@/tmp/phase8.torrent \ -F savepath=/downloads \ -F stopped=true >/dev/null; then break fi if (( attempt == 3 )); then return 1 fi sleep "$attempt" done local qb_hash qb_hash=$( control_adversarial qb-hash \ --endpoint "$(qb_endpoint "$node")" \ --name "$name" ) || return compose_node "$node" exec -T qbittorrent curl -fsS \ -X POST http://127.0.0.1:8080/api/v2/torrents/recheck \ --data-urlencode "hashes=$qb_hash" >/dev/null \ || return control_complex assert-qb \ --endpoint "$(qb_endpoint "$node")" \ --info-hash "$qb_hash" \ --selected '0' >/dev/null \ || return printf '%s' "$qb_hash" } assert_qb_by_name() { local node=$1 local name=$2 local qb_hash qb_hash=$(control_adversarial qb-hash \ --endpoint "$(qb_endpoint "$node")" \ --name "$name") control_complex assert-qb \ --endpoint "$(qb_endpoint "$node")" \ --info-hash "$qb_hash" \ --selected '0' \ --no-downloaded >/dev/null } restore_archive2=false restore_scheduler=false restore_archive2_client() { if [[ "$restore_archive2" == true ]]; then compose_node archive-2 up -d --force-recreate client >/dev/null restore_archive2=false fi } restore_test_environment() { restore_archive2_client if [[ "$restore_scheduler" == true ]]; then control_adversarial scheduler --action resume >/dev/null 2>&1 || true restore_scheduler=false fi } trap restore_test_environment EXIT for node in cache-1 cache-2 archive-1 archive-2; do for hash in \ "$v2_hash" "$v2_qb_prefix" "$hybrid_v1" "$hybrid_v2" \ "$hybrid_qb_prefix" "$stall_hash"; do delete_hash "$node" "$hash" done compose_node "$node" exec -T qbittorrent \ rm -f /downloads/v2.bin /downloads/hybrid.bin /downloads/fixture.bin done compose_node cache-1 exec -T --user 1001:1001 qbittorrent /bin/sh -c \ 'printf "phase8-v2-content\n" > /downloads/v2.bin' v2_source_qb_hash=$(install_torrent cache-1 v2.bin "$v2_torrent") control_complex transfer \ --operation archive \ --source-client cache-1 \ --target-client archive-1 \ --info-hash "$v2_hash" \ --selected '0' \ --expected-selection '0' \ --expected-delta '0' assert_qb_by_name archive-1 v2.bin control_adversarial assert-placement \ --client-id archive-1 \ --v2 "$v2_hash" \ --files '0' control_complex evict \ --cache-client cache-1 \ --info-hash "$v2_hash" control_complex assert-qb \ --endpoint "$(qb_endpoint cache-1)" \ --info-hash "$v2_source_qb_hash" \ --absent >/dev/null control_complex transfer \ --operation unarchive \ --source-client archive-1 \ --target-client cache-2 \ --info-hash "$v2_hash" \ --selected '0' \ --expected-selection '0' \ --expected-delta '0' assert_qb_by_name cache-2 v2.bin control_adversarial assert-placement \ --client-id cache-2 \ --v2 "$v2_hash" \ --files '0' compose_node cache-1 exec -T --user 1001:1001 qbittorrent /bin/sh -c \ 'printf "phase8-hybrid-content\n" > /downloads/hybrid.bin' hybrid_source_qb_hash=$(install_torrent cache-1 hybrid.bin "$hybrid_torrent") control_complex transfer \ --operation archive \ --source-client cache-1 \ --target-client archive-2 \ --info-hash "$hybrid_v1" \ --selected '0' \ --expected-selection '0' \ --expected-delta '0' assert_qb_by_name archive-2 hybrid.bin control_adversarial assert-placement \ --client-id archive-2 \ --v1 "$hybrid_v1" \ --v2 "$hybrid_v2" \ --files '0' control_complex evict \ --cache-client cache-1 \ --info-hash "$hybrid_v1" control_complex assert-qb \ --endpoint "$(qb_endpoint cache-1)" \ --info-hash "$hybrid_source_qb_hash" \ --absent >/dev/null control_complex transfer \ --operation unarchive \ --source-client archive-2 \ --target-client cache-2 \ --info-hash "$hybrid_v1" \ --selected '0' \ --expected-selection '0' \ --expected-delta '0' assert_qb_by_name cache-2 hybrid.bin control_adversarial assert-placement \ --client-id cache-2 \ --v1 "$hybrid_v1" \ --v2 "$hybrid_v2" \ --files '0' 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 fixture.bin "$stall_torrent" >/dev/null stall_job=$(control_complex drive \ --operation archive \ --source-client cache-1 \ --target-client archive-2 \ --info-hash "$stall_hash" \ --through source_stage) compose_node archive-2 stop client restore_archive2=true control_adversarial wait-client \ --client-id archive-2 \ --no-connected control_complex advance \ --job-id "$stall_job" \ --action syncthing_transfer >/dev/null sleep 6 control_adversarial mark-stalled \ --job-id "$stall_job" \ --minimum-overall 0.2 compose_node archive-2 start client control_adversarial wait-client --client-id archive-2 restore_archive2=false control_complex resume --job-id "$stall_job" assert_qb_by_name archive-2 fixture.bin delete_hash archive-2 "$stall_hash" compose_node archive-2 exec -T qbittorrent rm -f /downloads/fixture.bin ARCHIVE_2_CLIENT_CONFIG=./config/client-exhausted.toml \ compose_node archive-2 up -d --force-recreate client >/dev/null restore_archive2=true control_adversarial wait-client --client-id archive-2 disk_job=$(control_complex create-paused \ --operation archive \ --source-client cache-1 \ --target-client archive-2 \ --info-hash "$stall_hash") control_complex expect-failure \ --job-id "$disk_job" \ --contains 'insufficient free space' control_complex assert-qb \ --endpoint "$(qb_endpoint cache-1)" \ --info-hash "$stall_hash" \ --selected '0' >/dev/null restore_archive2_client control_adversarial wait-client --client-id archive-2 control_adversarial scheduler --action pause >/dev/null restore_scheduler=true compose_control run --rm protocol-probe control_adversarial scheduler --action resume >/dev/null restore_scheduler=false printf '%s\n' \ 'phase 8 adversarial matrix passed: v2/hybrid round trips, recoverable stall,' \ 'bounded disk exhaustion, and duplicate/reordered WebSocket job events'