Cover pause and resume in the e2e run
The idempotency assertion compared counter deltas between the two runs, but per-run counters are reset at the start of every run, so the second run's row already showed that run alone — the delta was negative and the script failed before reaching anything else. With that fixed, a second account of 3000 messages exercises the new stop path end to end: pause mid-folder, assert the task and the account settle into paused, resume, and assert the resumed run covers every message while skipping the ones the paused stretch had already copied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+94
-9
@@ -158,7 +158,8 @@ wait_test_ok() {
|
||||
wait_test_ok
|
||||
|
||||
wait_run_done() {
|
||||
for ((i = 1; i <= 60; i++)); do
|
||||
# Generous: the resume scenario re-scans and copies thousands of messages.
|
||||
for ((i = 1; i <= 600; i++)); do
|
||||
local status
|
||||
status=$(api GET "/api/tasks/${TASK_ID}" | jq -r '.task.status')
|
||||
if [[ "$status" == "done" ]]; then
|
||||
@@ -185,16 +186,100 @@ log "POST /run (second run, expect idempotency)"
|
||||
api POST "/api/tasks/${TASK_ID}/run" >/dev/null
|
||||
wait_run_done
|
||||
|
||||
# Counters are reset at the start of every run, so run 2's row shows run 2
|
||||
# alone: it must copy nothing and skip what run 1 already migrated.
|
||||
RES2=$(api GET "/api/tasks/${TASK_ID}")
|
||||
RUN2_COPIED_TOTAL=$(echo "$RES2" | jq -r '.accounts[0].copied')
|
||||
RUN2_SKIPPED_TOTAL=$(echo "$RES2" | jq -r '.accounts[0].skipped')
|
||||
RUN2_COPIED=$(echo "$RES2" | jq -r '.accounts[0].copied')
|
||||
RUN2_SKIPPED=$(echo "$RES2" | jq -r '.accounts[0].skipped')
|
||||
RUN2_ERRORS=$(echo "$RES2" | jq -r '.accounts[0].errors')
|
||||
RUN2_COPIED_DELTA=$((RUN2_COPIED_TOTAL - RUN1_COPIED))
|
||||
RUN2_SKIPPED_DELTA=$((RUN2_SKIPPED_TOTAL - RUN1_SKIPPED))
|
||||
log "run 2: copied_delta=$RUN2_COPIED_DELTA skipped_delta=$RUN2_SKIPPED_DELTA errors=$RUN2_ERRORS"
|
||||
log "run 2: copied=$RUN2_COPIED skipped=$RUN2_SKIPPED errors=$RUN2_ERRORS"
|
||||
|
||||
[[ "$RUN2_ERRORS" == "0" ]] || fail "run 2 had errors"
|
||||
[[ "$RUN2_COPIED_DELTA" -eq 0 ]] || fail "run 2 copied $RUN2_COPIED_DELTA new messages (expected 0, not idempotent)"
|
||||
[[ "$RUN2_SKIPPED_DELTA" -gt 0 ]] || fail "run 2 skipped delta is $RUN2_SKIPPED_DELTA (expected >0)"
|
||||
[[ "$RUN2_COPIED" -eq 0 ]] || fail "run 2 copied $RUN2_COPIED new messages (expected 0, not idempotent)"
|
||||
[[ "$RUN2_SKIPPED" -eq "$RUN1_COPIED" ]] ||
|
||||
fail "run 2 skipped $RUN2_SKIPPED of the $RUN1_COPIED messages run 1 copied"
|
||||
|
||||
log "PASS: run1 copied=$RUN1_COPIED skipped=$RUN1_SKIPPED; run2 copied=$RUN2_COPIED_DELTA skipped=$RUN2_SKIPPED_DELTA (idempotent)"
|
||||
log "run1 copied=$RUN1_COPIED skipped=$RUN1_SKIPPED; run2 copied=$RUN2_COPIED skipped=$RUN2_SKIPPED (idempotent)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pause / resume: a second account with enough messages that the run is still
|
||||
# in flight when the pause lands. Pausing must leave the account resumable, and
|
||||
# resuming must finish it without re-copying what the first stretch already did.
|
||||
# ---------------------------------------------------------------------------
|
||||
SRC_USER2="src2@example.com"
|
||||
DST_USER2="dst2@example.com"
|
||||
# Large enough that the copy is still in flight when the pause lands — greenmail
|
||||
# on a local socket copies well over a thousand small messages per second.
|
||||
SEED_COUNT=3000
|
||||
|
||||
log "seeding ${SEED_COUNT} messages into ${SRC_USER2} INBOX (pause/resume scenario)"
|
||||
python3 "$SEED_PY" 127.0.0.1 3143 "$SRC_USER2" "$MAIL_PASS" "$SEED_COUNT"
|
||||
|
||||
log "adding second account (src2 -> dst2)"
|
||||
ACCOUNT2_ID=$(api POST "/api/tasks/${TASK_ID}/accounts" \
|
||||
"{\"src_login\":\"${SRC_USER2}\",\"src_pass\":\"${MAIL_PASS}\",\"dst_login\":\"${DST_USER2}\",\"dst_pass\":\"${MAIL_PASS}\"}" | jq -r .id)
|
||||
[[ "$ACCOUNT2_ID" =~ ^[0-9]+$ ]] || fail "bad second account id: $ACCOUNT2_ID"
|
||||
log "account2_id=$ACCOUNT2_ID"
|
||||
|
||||
log "POST /test (both accounts)"
|
||||
api POST "/api/tasks/${TASK_ID}/test" >/dev/null
|
||||
for ((i = 1; i <= 30; i++)); do
|
||||
BOTH_OK=$(api GET "/api/tasks/${TASK_ID}" |
|
||||
jq -r '[.accounts[] | select(.test_src_status=="ok" and .test_dst_status=="ok")] | length')
|
||||
[[ "$BOTH_OK" == "2" ]] && break
|
||||
sleep 1
|
||||
done
|
||||
[[ "$BOTH_OK" == "2" ]] || fail "second account did not pass connection tests (ok count=$BOTH_OK)"
|
||||
|
||||
# Account view for account2, by id.
|
||||
acct2() { api GET "/api/tasks/${TASK_ID}" | jq -r ".accounts[] | select(.id==${ACCOUNT2_ID}) | $1"; }
|
||||
|
||||
log "POST /run (account2 only)"
|
||||
api POST "/api/tasks/${TASK_ID}/run" "{\"account_ids\":[${ACCOUNT2_ID}]}" >/dev/null
|
||||
|
||||
# Per-account counters are only written to the DB when a folder completes, so
|
||||
# "copied so far" is invisible here — wait for the account to go running, give
|
||||
# the copy a few seconds of real work, then pause mid-folder.
|
||||
log "waiting for account2 to start running"
|
||||
for ((i = 1; i <= 120; i++)); do
|
||||
[[ "$(acct2 .status)" == "running" ]] && break
|
||||
sleep 0.5
|
||||
done
|
||||
[[ "$(acct2 .status)" == "running" ]] || fail "account2 never reached running"
|
||||
|
||||
log "letting it copy for a few seconds, then pausing mid-folder"
|
||||
sleep 5
|
||||
curl -fsS -b "$COOKIE_JAR" -c "$COOKIE_JAR" -X POST "$BASE/api/tasks/${TASK_ID}/pause" >/dev/null ||
|
||||
fail "pause rejected — the run finished before the pause landed, seed more messages"
|
||||
|
||||
log "waiting for the task to settle into paused"
|
||||
for ((i = 1; i <= 60; i++)); do
|
||||
TASK_STATUS=$(api GET "/api/tasks/${TASK_ID}" | jq -r '.task.status')
|
||||
[[ "$TASK_STATUS" == "paused" ]] && break
|
||||
sleep 1
|
||||
done
|
||||
[[ "$TASK_STATUS" == "paused" ]] || fail "task status=$TASK_STATUS after pause (expected paused)"
|
||||
|
||||
ACC2_STATUS=$(acct2 .status)
|
||||
[[ "$ACC2_STATUS" == "paused" ]] || fail "account2 status=$ACC2_STATUS after pause (expected paused)"
|
||||
log "paused (folder-level counters at copied=$(acct2 .copied) of $SEED_COUNT)"
|
||||
|
||||
log "POST /resume"
|
||||
api POST "/api/tasks/${TASK_ID}/resume" >/dev/null
|
||||
wait_run_done
|
||||
|
||||
RESUMED_COPIED=$(acct2 .copied)
|
||||
RESUMED_SKIPPED=$(acct2 .skipped)
|
||||
RESUMED_ERRORS=$(acct2 .errors)
|
||||
RESUMED_TOTAL=$((RESUMED_COPIED + RESUMED_SKIPPED))
|
||||
log "after resume: copied=$RESUMED_COPIED skipped=$RESUMED_SKIPPED errors=$RESUMED_ERRORS"
|
||||
[[ "$RESUMED_ERRORS" == "0" ]] || fail "resumed run had errors"
|
||||
# Counters reset per run, so the resumed run alone must account for every
|
||||
# message: the ones it copied now plus the ones the paused stretch already did.
|
||||
[[ "$RESUMED_TOTAL" -eq "$SEED_COUNT" ]] || fail "resumed run covered $RESUMED_TOTAL of $SEED_COUNT messages"
|
||||
# Non-zero skipped is the proof that the paused stretch's work survived: those
|
||||
# messages are in the migration journal, so the resume did not re-copy them.
|
||||
[[ "$RESUMED_SKIPPED" -gt 0 ]] ||
|
||||
fail "resumed run skipped nothing — the paused stretch's progress was lost"
|
||||
|
||||
log "PASS: idempotent re-run; pause was resumable, resume re-copied $RESUMED_COPIED and skipped $RESUMED_SKIPPED of $SEED_COUNT"
|
||||
|
||||
Reference in New Issue
Block a user