From 8fe12d3eff17b0ca701099e4c41b74ce304b58a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Sun, 19 Jul 2026 04:35:01 +0200 Subject: [PATCH] release 1.15.0: CHF fake-incoming ladder (23 rungs to 50k) devtesting runs a log-spaced amount ladder up to CHF:50000 after the baseline plumbing probe. Each rung uses geniban debit funding. Defaults DEVTESTING_LADDER=1 STEPS=23 MAX=50000; stage RUN_TIMEOUT example 1800s. --- README.md | 5 +- TESTS.md | 3 +- VERSION | 2 +- VERSIONS.md | 1 + check_devtesting.sh | 292 +++++++++++++++++++----- host-agent/env/mytops-stage.env.example | 8 +- taler-monitoring.sh | 11 +- 7 files changed, 257 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 79c4ffa..81802c9 100644 --- a/README.md +++ b/README.md @@ -433,7 +433,10 @@ Taler Operations **rusty** host (`taler-devtesting` forced command): ``` Needs SSH to `devtesting@rusty.taler-ops.ch` (Host `taler-rusty-devtesting`). -Probes: CLI help, `geniban` (CH IBAN), `fake-incoming` CHF to exchange credit +Probes: CLI help, `geniban` (CH IBAN / synthetic debit funding), baseline `fake-incoming`, +optional **amount ladder** (`DEVTESTING_LADDER=1`, default **23** rungs up to **CHF:50000**). +Each rung reuses geniban debit payto so volume covers the max rung without a real bank withdraw. +`fake-incoming` CHF to exchange credit IBAN. A nexus bounce *missing reserve public key* is **OK** for the plumbing probe (no real wallet reserve required). diff --git a/TESTS.md b/TESTS.md index 9826fd7..c88506e 100644 --- a/TESTS.md +++ b/TESTS.md @@ -202,4 +202,5 @@ Always use `printf --` friendly plain headers when colour is off (leading `---` | **devtesting.cli-** | SSH to rusty · `taler-devtesting` CLI | | **devtesting.geniban-** | synthetic CH IBAN | | **devtesting.credit-payto-** | exchange credit IBAN for nexus | -| **devtesting.fake-incoming-** | CHF IN via nexus; bounce missing reserve pub = plumbing OK | +| **devtesting.fake-incoming-** / **baseline-** | CHF IN via nexus; bounce missing reserve pub = plumbing OK | +| **devtesting.ladder-** | CHF fake-incoming amount ladder (default 23 rungs → CHF:50000; synthetic geniban debit each rung) | diff --git a/VERSION b/VERSION index 850e742..141f2e8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.14.0 +1.15.0 diff --git a/VERSIONS.md b/VERSIONS.md index b1e03e0..f1ac38d 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -17,6 +17,7 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre | Tag | Date (UTC) | Notes | |-----|------------|--------| +| **v1.15.0** | 2026-07-19 | **Feature:** CHF **fake-incoming amount ladder** in `devtesting` (`DEVTESTING_LADDER=1`, default **23** log-spaced rungs **CHF:0.01 → CHF:50000**); synthetic geniban debit funds each rung; baseline plumbing probe kept. | | **v1.14.0** | 2026-07-19 | **Feature:** SUMMARY box rows are **colour-distinct** (per-severity badge + tinted background + bold counts; verdict-coloured header); HTML mon pages style `sum-ok` / `sum-warn` / `sum-err` / … panels the same way. | | **v1.13.13** | 2026-07-19 | **Bugfix:** progress estimate for **devtesting** / fake-franken is 8 (happy-path numbered checks), not 10 — avoids mid-run 42/48 then end snap 46/46. | | **v1.13.12** | 2026-07-19 | **Bugfix:** SUMMARY chrome (`[ WARN ] 2`, `warnings only`, OK/ERROR counts without `#NNN`) is **meta**, not warn/error — sticky counts + warn filter no longer double SUMMARY rows; filter context skips blank lines. | diff --git a/check_devtesting.sh b/check_devtesting.sh index 088ddd7..a4723cb 100755 --- a/check_devtesting.sh +++ b/check_devtesting.sh @@ -6,18 +6,21 @@ # # Probes: # 1) CLI reachable (fake-incoming --help) -# 2) geniban → Swiss IBAN -# 3) fake-incoming CHF amount to exchange credit IBAN (nexus IN) -# Bounce "missing reserve public key" = plumbing OK (no real wallet reserve) -# Full credit needs a real reserve pub as --subject (optional later) +# 2) geniban → Swiss IBAN (debit side — “enough money” is synthetic per rung) +# 3) baseline fake-incoming CHF:1.00 (plumbing) +# 4) optional amount ladder: DEVTESTING_LADDER=1 (default) — 23 rungs up to 50k CHF # # Env: -# DEVTESTING_SSH default taler-rusty-devtesting (or user@host) -# DEVTESTING_AMOUNT default CHF:1.00 -# DEVTESTING_CREDIT_PAYTO exchange IBAN payto (auto from EXCHANGE_PUBLIC/keys if empty) -# DEVTESTING_EXCHANGE_IBAN fallback CH6808573105529100001 (TOPS CHF exchange) -# DEVTESTING_SKIP=1 skip phase -# SKIP_SSH=1 skip (same as other SSH phases) +# DEVTESTING_SSH default taler-rusty-devtesting (or user@host) +# DEVTESTING_AMOUNT baseline amount (default CHF:1.00) +# DEVTESTING_CREDIT_PAYTO exchange IBAN payto +# DEVTESTING_EXCHANGE_IBAN fallback CH6808573105529100001 (TOPS CHF exchange) +# DEVTESTING_LADDER 1 (default) run amount ladder; 0 = baseline only +# DEVTESTING_LADDER_STEPS default 23 +# DEVTESTING_LADDER_MAX default 50000 (CHF major units) +# DEVTESTING_LADDER_MIN default 0.01 +# DEVTESTING_LADDER_STOP_ON_FAIL 1 (default) stop ladder on hard fail; 0 = continue +# DEVTESTING_SKIP=1 skip phase # set -euo pipefail ROOT=$(cd "$(dirname "$0")" && pwd) @@ -37,8 +40,13 @@ fi : "${DEVTESTING_SSH:=}" : "${DEVTESTING_AMOUNT:=CHF:1.00}" : "${DEVTESTING_EXCHANGE_IBAN:=CH6808573105529100001}" +: "${DEVTESTING_LADDER:=1}" +: "${DEVTESTING_LADDER_STEPS:=23}" +: "${DEVTESTING_LADDER_MAX:=50000}" +: "${DEVTESTING_LADDER_MIN:=0.01}" +: "${DEVTESTING_LADDER_STOP_ON_FAIL:=1}" : "${SSH_CONNECT_TIMEOUT:=8}" -: "${SSH_CMD_TIMEOUT:=45}" +: "${SSH_CMD_TIMEOUT:=60}" ssh_dt() { # Prefer Host alias; fall back to explicit user@host if config missing @@ -61,8 +69,57 @@ ssh_dt() { fi } +# Run one fake-incoming. Sets globals: FI_RC FI_FLAT FI_STATUS +# FI_STATUS: ok_bounce | ok_in | ok_cli | fail_ssh | fail_creditor | fail_nexus | fail_other +run_fake_incoming() { + local amount="$1" subject="$2" credit="$3" debit="$4" + local fi_out + FI_RC=0 + FI_FLAT="" + FI_STATUS="fail_other" + fi_out=$(ssh_dt fake-incoming \ + --amount "${amount}" \ + --subject "${subject}" \ + --credit-payto "${credit}" \ + --debit-payto "${debit}" 2>&1) || FI_RC=$? + FI_FLAT=$(printf '%s' "$fi_out" | tr '\n' ' ') + + if printf '%s' "$FI_FLAT" | grep -qiE 'Permission denied|Connection refused|Could not resolve|No such command'; then + FI_STATUS=fail_ssh + return 1 + fi + if printf '%s' "$FI_FLAT" | grep -qiE 'Creditor must be the exchange'; then + FI_STATUS=fail_creditor + return 1 + fi + if printf '%s' "$FI_FLAT" | grep -qiE 'Missing amount|unable to read secrets|CalledProcessError'; then + if ! printf '%s' "$FI_FLAT" | grep -qE 'libeufin-nexus - IN .* CHF'; then + FI_STATUS=fail_nexus + return 1 + fi + fi + if printf '%s' "$FI_FLAT" | grep -qE 'libeufin-nexus - IN .* CHF:[0-9]'; then + if printf '%s' "$FI_FLAT" | grep -qiE 'bounced.*missing reserve public key'; then + FI_STATUS=ok_bounce + return 0 + fi + if printf '%s' "$FI_FLAT" | grep -qiE 'bounced'; then + FI_STATUS=ok_bounce + return 0 + fi + FI_STATUS=ok_in + return 0 + fi + if [ "$FI_RC" -eq 0 ] && printf '%s' "$FI_FLAT" | grep -qi 'Faking incoming CHF'; then + FI_STATUS=ok_cli + return 0 + fi + FI_STATUS=fail_other + return 1 +} + set_group cli -info "ssh" "DEVTESTING_SSH=${DEVTESTING_SSH} amount=${DEVTESTING_AMOUNT}" +info "ssh" "DEVTESTING_SSH=${DEVTESTING_SSH} baseline=${DEVTESTING_AMOUNT} ladder=${DEVTESTING_LADDER} steps=${DEVTESTING_LADDER_STEPS} max=CHF:${DEVTESTING_LADDER_MAX}" help_out=$(ssh_dt fake-incoming --help 2>&1) || true if printf '%s' "$help_out" | grep -q 'fake-incoming'; then @@ -77,7 +134,7 @@ set_group geniban iban_out=$(ssh_dt geniban 2>&1) || true iban=$(printf '%s\n' "$iban_out" | grep -E '^CH[0-9A-Z]+$' | tail -1 || true) if [ -n "$iban" ]; then - ok "geniban" "synthetic IBAN $iban" + ok "geniban" "synthetic IBAN $iban (debit / “withdraw” side for each rung)" else fail "geniban" "expected CH… IBAN" "$(printf '%s' "$iban_out" | tr '\n' ' ' | head -c 160)" exit 1 @@ -92,55 +149,174 @@ if [ -z "$credit_payto" ]; then fi ok "credit-payto" "$credit_payto" -# Subject: not a real reserve — nexus should bounce missing reserve public key -# (proves CHF fake-incoming path). Real reserve pub would credit wirewatch. -subject="mon-fake-franken-$(date -u +%Y%m%d%H%M%S)-$$" debit_payto="payto://iban/${iban}?receiver-name=MonDevTest" +ts_base=$(date -u +%Y%m%d%H%M%S) -set_group fake-incoming +# --- baseline plumbing probe --- +set_group baseline +subject="mon-fake-franken-${ts_base}-base-$$" info "fake-incoming" "amount=${DEVTESTING_AMOUNT} subject=${subject}" -fi_rc=0 -fi_out=$(ssh_dt fake-incoming \ - --amount "${DEVTESTING_AMOUNT}" \ - --subject "${subject}" \ - --credit-payto "${credit_payto}" \ - --debit-payto "${debit_payto}" 2>&1) || fi_rc=$? - -# Normalize multi-line for matching -fi_flat=$(printf '%s' "$fi_out" | tr '\n' ' ') - -if printf '%s' "$fi_flat" | grep -qiE 'Permission denied|Connection refused|Could not resolve|No such command'; then - fail "fake-incoming" "SSH/CLI failure" "$(printf '%s' "$fi_flat" | head -c 200)" - exit 1 -fi - -if printf '%s' "$fi_flat" | grep -qiE 'Creditor must be the exchange'; then - fail "fake-incoming" "wrong credit IBAN (not exchange)" "$(printf '%s' "$fi_flat" | head -c 220)" - exit 1 -fi - -if printf '%s' "$fi_flat" | grep -qiE 'Missing amount|unable to read secrets|CalledProcessError'; then - # secrets warn alone is soft if we still see IN line - if ! printf '%s' "$fi_flat" | grep -qE 'libeufin-nexus - IN .* CHF'; then - fail "fake-incoming" "nexus rejected request" "$(printf '%s' "$fi_flat" | head -c 220)" - exit 1 - fi -fi - -if printf '%s' "$fi_flat" | grep -qE 'libeufin-nexus - IN .* CHF:[0-9]'; then - if printf '%s' "$fi_flat" | grep -qiE 'bounced.*missing reserve public key'; then - ok "fake-incoming" "CHF IN accepted by nexus · bounced (no reserve pub) — fake-franken plumbing OK" - info "note" "use real reserve pub as --subject for wirewatch credit (not required for mon probe)" - elif printf '%s' "$fi_flat" | grep -qiE 'bounced'; then - warn "fake-incoming" "CHF IN bounced (other reason)" "$(printf '%s' "$fi_flat" | grep -oiE 'bounced[^ ]* [^ ]*' | head -c 160)" - else - ok "fake-incoming" "CHF IN recorded by nexus (no bounce line)" - fi -elif [ "$fi_rc" -eq 0 ] && printf '%s' "$fi_flat" | grep -qi 'Faking incoming CHF'; then - ok "fake-incoming" "CLI completed CHF fake-incoming" +if run_fake_incoming "$DEVTESTING_AMOUNT" "$subject" "$credit_payto" "$debit_payto"; then + case "$FI_STATUS" in + ok_bounce) + ok "fake-incoming" "CHF IN accepted by nexus · bounce/missing reserve pub — plumbing OK" + info "note" "use real reserve pub as --subject for wirewatch credit (not required for mon probe)" + ;; + ok_in) ok "fake-incoming" "CHF IN recorded by nexus (no bounce line)" ;; + ok_cli) ok "fake-incoming" "CLI completed CHF fake-incoming" ;; + *) ok "fake-incoming" "status=${FI_STATUS}" ;; + esac else - fail "fake-incoming" "no CHF IN line from nexus" "rc=${fi_rc} $(printf '%s' "$fi_flat" | head -c 200)" - exit 1 + case "$FI_STATUS" in + fail_ssh) + fail "fake-incoming" "SSH/CLI failure" "$(printf '%s' "$FI_FLAT" | head -c 200)" + exit 1 + ;; + fail_creditor) + fail "fake-incoming" "wrong credit IBAN (not exchange)" "$(printf '%s' "$FI_FLAT" | head -c 220)" + exit 1 + ;; + fail_nexus) + fail "fake-incoming" "nexus rejected request" "$(printf '%s' "$FI_FLAT" | head -c 220)" + exit 1 + ;; + *) + fail "fake-incoming" "no CHF IN line from nexus" "rc=${FI_RC} $(printf '%s' "$FI_FLAT" | head -c 200)" + exit 1 + ;; + esac +fi + +# --- CHF amount ladder (fake-incoming rungs) --- +if [ "${DEVTESTING_LADDER}" = "1" ]; then + set_group ladder + section "devtesting · CHF fake-incoming ladder (→ ${DEVTESTING_LADDER_MAX})" + + LADDER_FILE=$(mktemp "${TMPDIR:-/tmp}/devtesting-ladder.XXXXXX") + python3 - <<'PY' "${DEVTESTING_LADDER_MAX}" "${DEVTESTING_LADDER_STEPS}" "${DEVTESTING_LADDER_MIN}" "$LADDER_FILE" +import math, sys +from decimal import Decimal, ROUND_HALF_UP +from pathlib import Path + +max_amt = Decimal(sys.argv[1]) +steps = max(1, int(sys.argv[2])) +min_amt = Decimal(sys.argv[3]) +out = Path(sys.argv[4]) +if min_amt <= 0: + min_amt = Decimal("0.01") +if min_amt >= max_amt: + min_amt = max_amt / Decimal(1000) +# CHF: 2 decimal places +q2 = Decimal("0.01") + +def quant(v: Decimal) -> Decimal: + if v < min_amt: + v = min_amt + return v.quantize(q2, rounding=ROUND_HALF_UP) + +def fmt(v: Decimal) -> str: + q = quant(v) + if q == q.to_integral(): + return str(int(q)) + return format(q, "f") + +if steps == 1: + amts = [quant(max_amt)] +elif steps == 2: + amts = [quant(min_amt), quant(max_amt)] +else: + # Exactly `steps` log-spaced rungs from min → max (includes both ends). + # Guarantees max rung = DEVTESTING_LADDER_MAX (“enough” volume at the top). + amts = [] + lo, hi = float(min_amt), float(max_amt) + if hi <= lo: + hi = lo * 10 + for i in range(steps): + if steps == 1: + v = max_amt + else: + t = i / (steps - 1) + v = Decimal(str(math.exp(math.log(lo) + t * (math.log(hi) - math.log(lo))))) + amts.append(quant(v)) + amts[0] = quant(min_amt) + amts[-1] = quant(max_amt) + # enforce strictly increasing + for i in range(1, len(amts)): + if amts[i] <= amts[i - 1]: + amts[i] = quant(amts[i - 1] + min_amt) + amts[-1] = quant(max_amt) + if amts[-1] <= amts[-2]: + # last-but-one squeezed; leave max exact and drop collision by nudging previous + amts[-2] = quant(max(amts[-3] + min_amt if len(amts) > 2 else min_amt, max_amt - Decimal(1))) + if amts[-2] >= amts[-1]: + amts[-2] = quant(amts[-1] - min_amt) + +lines = ["CHF:%s" % fmt(a) for a in amts] +out.write_text("\n".join(lines) + "\n") +PY + n_rungs=$(grep -cE '^CHF:' "$LADDER_FILE" || echo 0) + ladder_sum=$(python3 -c ' +from decimal import Decimal +s=Decimal(0) +for ln in open("'"$LADDER_FILE"'"): + ln=ln.strip() + if not ln: continue + s += Decimal(ln.split(":",1)[-1]) +print(s) +' 2>/dev/null || echo "?") + + info "ladder plan" "steps=${n_rungs} max=CHF:${DEVTESTING_LADDER_MAX} min=CHF:${DEVTESTING_LADDER_MIN} sum≈CHF:${ladder_sum} (synthetic debit geniban each rung)" + info "ladder funding" "each rung uses geniban debit payto — no real bank withdraw; volume covers max rung CHF:${DEVTESTING_LADDER_MAX}" + + LADDER_OK=0 + LADDER_FAIL=0 + LADDER_WARN=0 + rung=0 + while IFS= read -r amt || [ -n "$amt" ]; do + [ -n "$amt" ] || continue + rung=$((rung + 1)) + subject="mon-fake-ladder-${ts_base}-r${rung}-$$" + info "rung ${rung}/${n_rungs}" "amount=${amt} subject=${subject}" + if run_fake_incoming "$amt" "$subject" "$credit_payto" "$debit_payto"; then + case "$FI_STATUS" in + ok_bounce) + ok "rung ${rung}" "${amt} · nexus IN + bounce (plumbing OK)" + LADDER_OK=$((LADDER_OK + 1)) + ;; + ok_in) + ok "rung ${rung}" "${amt} · nexus IN recorded" + LADDER_OK=$((LADDER_OK + 1)) + ;; + ok_cli) + ok "rung ${rung}" "${amt} · CLI ok" + LADDER_OK=$((LADDER_OK + 1)) + ;; + *) + warn "rung ${rung}" "${amt} · status=${FI_STATUS}" + LADDER_WARN=$((LADDER_WARN + 1)) + ;; + esac + else + fail "rung ${rung}" "${amt} · ${FI_STATUS}" "$(printf '%s' "$FI_FLAT" | head -c 180)" + LADDER_FAIL=$((LADDER_FAIL + 1)) + if [ "${DEVTESTING_LADDER_STOP_ON_FAIL}" = "1" ]; then + warn "ladder stop" "DEVTESTING_LADDER_STOP_ON_FAIL=1 after rung ${rung}" + break + fi + fi + done <"$LADDER_FILE" + rm -f "$LADDER_FILE" + + info "ladder summary" "ok=${LADDER_OK} warn=${LADDER_WARN} fail=${LADDER_FAIL} / planned=${n_rungs} · max=CHF:${DEVTESTING_LADDER_MAX}" + if [ "$LADDER_FAIL" -gt 0 ] && [ "$LADDER_OK" -eq 0 ]; then + fail "ladder" "all rungs failed" + elif [ "$LADDER_FAIL" -gt 0 ]; then + warn "ladder" "partial failures fail=${LADDER_FAIL} ok=${LADDER_OK}" + else + ok "ladder complete" "ok=${LADDER_OK} through max CHF:${DEVTESTING_LADDER_MAX}" + fi +else + info "ladder" "skipped (DEVTESTING_LADDER=0)" fi info "done" "devtesting fake-franken probe finished" diff --git a/host-agent/env/mytops-stage.env.example b/host-agent/env/mytops-stage.env.example index 5db18bf..0da4f19 100644 --- a/host-agent/env/mytops-stage.env.example +++ b/host-agent/env/mytops-stage.env.example @@ -18,7 +18,8 @@ SKIP_SSH=1 LOCAL_STACK=0 INSIDE_PODMAN=0 CONTINUE_ON_ERROR=1 -RUN_TIMEOUT=600 +# ladder needs wall time (23× fake-incoming SSH) +RUN_TIMEOUT=1800 STRICT_EXIT=1 MONPAGES_REQUIRE_PUBLIC=1 MONPAGES_INVENTORY=job @@ -32,4 +33,9 @@ CHECK_LANDING=0 # rusty devtesting (SSH Host taler-rusty-devtesting → devtesting@rusty.taler-ops.ch) DEVTESTING_SSH=taler-rusty-devtesting DEVTESTING_AMOUNT=CHF:1.00 +# CHF fake-incoming amount ladder (23 rungs → 50k) +DEVTESTING_LADDER=1 +DEVTESTING_LADDER_STEPS=23 +DEVTESTING_LADDER_MAX=50000 +DEVTESTING_LADDER_MIN=0.01 # DEVTESTING_SKIP=1 diff --git a/taler-monitoring.sh b/taler-monitoring.sh index dd2b6d4..67ddf76 100755 --- a/taler-monitoring.sh +++ b/taler-monitoring.sh @@ -448,9 +448,14 @@ _progress_estimate_phase() { ;; mattermost) n=25 ;; mail) n=40 ;; - # Happy path: ssh + cli + geniban + credit-payto + fake-incoming×2 + note + done ≈ 8 - # (was 10 → mid-run 42/48 then snap 46/46 after last phase) - devtesting|franken|fake-franken|fake_franken) n=8 ;; + # baseline ~8 + ladder rungs (default 23 × ~2 lines) + summary + devtesting|franken|fake-franken|fake_franken) + n=8 + if [ "${DEVTESTING_LADDER:-1}" != "0" ]; then + _ls="${DEVTESTING_LADDER_STEPS:-23}" + n=$((8 + _ls * 2 + 6)) + fi + ;; *) n=15 ;; esac printf '%s' "$n"