#!/usr/bin/env bash # erp#44 acceptance — LIVE double-apply on the SANDBOX: applying the same # manifest twice must be a no-op the second time (all ops deduped, zero new # rows). This is the replay that was IMPOSSIBLE after the 2026-07-11 rehearsal's # manifest B failed mid-run (Learning #4: re-applying would have duplicated the # DARNIS invoice). # # What it does (writes go ONLY through the host-guarded dol-write.sh): # 1. builds a small self-contained manifest with a unique-per-run fixture: # one supplier thirdparty + one validated supplier invoice (Qonto-style # transaction id, so the erp#37 normalization is exercised too) + one # payment + one GED attach (erp#43: object_id via @ref, file path relative # to the manifest, idempotent by sha256) # 2. applies it → expects 4 created, no dedupe # 3. applies it AGAIN → expects 4 deduped, zero new rows (verified by # row-counting thirdparties / invoices / payments / GED files via the API) # # Sandbox etiquette: the fixture rows stay behind (the sandbox is disposable; # a checkpoint refresh reclaims them). Run from anywhere: # .claude/skills/dolibarr-sandbox-write/tests/replay-idempotency.sh [evidence-dir] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPTS="${SCRIPT_DIR}/../scripts" W="${SCRIPTS}/dol-write.sh" # deliberately NOT $DOL_WRITE — this test is live unset DOL_WRITE || true EV="${1:-$(mktemp -d -t idem44.XXXXXX)}" mkdir -p "${EV}" fail() { echo "FAIL: $*" >&2; exit 1; } STAMP="$(date +%Y%m%d-%H%M%S)" NAME="IDEM44 Replay Fixture ${STAMP}" REFSUP="IDEM44-${STAMP}" TX="arcodange-idem44-transaction-${STAMP}-e2e-replay" # Qonto-style long form TX_SHORT="${STAMP}-e2e-replay" # its normalized num cat > "${EV}/manifest.json" < "${EV}/attach-fixture.pdf" count_rows() { # $1 = path, counts a JSON array (Dolibarr 404-on-empty => 0) local out if out="$("${W}" GET "$1" 2>/dev/null)"; then python3 -c "import json,sys; r=json.load(sys.stdin); print(len(r) if isinstance(r,list) else 0)" <<<"${out}" else echo 0 fi } FLT="$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(\"(t.nom:=:'%s')\" % sys.argv[1]))" "${NAME}")" tp_count() { count_rows "/thirdparties?limit=100&sqlfilters=${FLT}"; } inv_count() { count_rows "/supplierinvoices?thirdparty_ids=$1&limit=500"; } pay_count() { count_rows "/supplierinvoices/$1/payments"; } doc_count() { # GED files on a supplier invoice (404 = none yet => 0) local out if out="$("${W}" GET "/documents?modulepart=facture_fournisseur&id=$1" 2>/dev/null)"; then python3 -c "import json,sys; r=json.load(sys.stdin); print(len([x for x in r if x.get('type')=='file']) if isinstance(r,list) else 0)" <<<"${out}" else echo 0 fi } echo "== erp#44 replay-idempotency — fixture ${STAMP} (evidence: ${EV}) ==" [[ "$(tp_count)" == "0" ]] || fail "fixture name already exists on the sandbox (clock collision?)" echo; echo "-- RUN 1: expect 4 created ------------------------------------------------" "${SCRIPTS}/promote-apply.sh" "${EV}/manifest.json" --target sandbox \ | tee "${EV}/run1.out" grep -q 'deduped' "${EV}/run1.out" && fail "run 1: nothing may dedupe on a fresh fixture" [[ "$(grep -c ' created' "${EV}/run1.out")" == "4" ]] || fail "run 1: expected 4 created ops" grep -q '(4 created)' "${EV}/run1.out" || fail "run 1: summary must say (4 created)" TPID="$(python3 -c "import json,sys,re m=re.search(r'ref -> id: (\{.*\})', open(sys.argv[1]).read()); print(json.loads(m.group(1))['tp'])" "${EV}/run1.out")" INVID="$(python3 -c "import json,sys,re m=re.search(r'ref -> id: (\{.*\})', open(sys.argv[1]).read()); print(json.loads(m.group(1))['inv'])" "${EV}/run1.out")" TP1="$(tp_count)"; INV1="$(inv_count "${TPID}")"; PAY1="$(pay_count "${INVID}")"; DOC1="$(doc_count "${INVID}")" echo "row counts after run 1: thirdparties=${TP1} invoices=${INV1} payments=${PAY1} ged_files=${DOC1}" | tee "${EV}/counts-run1.txt" [[ "${TP1}" == "1" && "${INV1}" == "1" && "${PAY1}" == "1" && "${DOC1}" == "1" ]] || fail "run 1 must have created exactly 1 of each" echo; echo "-- RUN 2 (same manifest): expect 4 deduped, zero new rows -----------------" "${SCRIPTS}/promote-apply.sh" "${EV}/manifest.json" --target sandbox \ | tee "${EV}/run2.out" [[ "$(grep -c 'deduped=true' "${EV}/run2.out")" == "4" ]] || fail "run 2: all 4 ops must dedupe" grep -q ' created' "${EV}/run2.out" && fail "run 2: nothing may be created on a replay" grep -q '(4 deduped)' "${EV}/run2.out" || fail "run 2: summary must say (4 deduped)" TP2="$(tp_count)"; INV2="$(inv_count "${TPID}")"; PAY2="$(pay_count "${INVID}")"; DOC2="$(doc_count "${INVID}")" echo "row counts after run 2: thirdparties=${TP2} invoices=${INV2} payments=${PAY2} ged_files=${DOC2}" | tee "${EV}/counts-run2.txt" [[ "${TP2}" == "${TP1}" && "${INV2}" == "${INV1}" && "${PAY2}" == "${PAY1}" && "${DOC2}" == "${DOC1}" ]] \ || fail "run 2 must add ZERO rows (run1: ${TP1}/${INV1}/${PAY1}/${DOC1}, run2: ${TP2}/${INV2}/${PAY2}/${DOC2})" # The payment's stored num must be the erp#37 canonical short form. "${W}" GET "/supplierinvoices/${INVID}/payments" > "${EV}/payments.json" grep -q "\"${TX_SHORT}\"" "${EV}/payments.json" \ || fail "stored num must be the normalized short form ${TX_SHORT}" echo echo "PASS: replay is a no-op — run 1 created 4 rows (tp=${TPID}, inv=${INVID}), run 2 deduped all 4, row counts unchanged (${TP2}/${INV2}/${PAY2}/${DOC2})." echo "Evidence in ${EV}: manifest.json attach-fixture.pdf run1.out run2.out counts-run*.txt payments.json"