Files
bal-electrum-plugin/tests/sim_update_flows.py
GenSpark AI Developer 365824767b fix(plugin): missed-update fixes, server re-check, and bold Building Will results (v0.3.2)
Revert the v0.3.1 double-invalidation change (it caused an inheritance-list
regression: stale/invalidated wills lingered and heir/date updates became
incoherent) and add several targeted missed-update fixes plus a UI refinement.

Revert (v0.3.1 -> v0.3.2):
- Remove Will.mark_invalidated_by_tx() and its call in
  loop_broadcast_invalidating. core/will.py and gui/qt/dialogs.py are restored
  to the working v0.3.0 behaviour. The postpone double-invalidation issue is
  intentionally left open, to be addressed without touching the shared
  broadcast path.

FIX 1 - detect heir removal on Check / Electrum close:
- core/will.py (check_willexecutors_and_heirs): the else-branch now raises
  HeirNotFoundException when a will still carries an heir that is no longer in
  the current heirs set (heir removed), mirroring the existing 'heir added'
  path. Rebuild therefore triggers on Check and on_close (same build_will_task
  path), as decided by the user (manual update only, no auto-rebuild).

FIX 2 - Check queries servers for already-sent wills:
- core/will.py: new Will.needs_server_check(w) returns True for any VALID will
  with a will-executor that is not yet CHECKED (no longer limited to PUSHED).
- gui/qt/lists.py (PreviewList.check): use needs_server_check so wills stuck on
  'New / Not sent' are re-checked instead of reporting 'nothing to do'.

FIX 3 - Settings-dialog hide toggles refresh the list:
- core/plugin_base.py: new sync_hide_filters() re-reads the cached
  _hide_invalidated / _hide_replaced flags from the persisted config.
- gui/qt/window.py (update_all): call sync_hide_filters() before refreshing, so
  toggling 'Hide Invalidated' / 'Hide Replaced' in the Settings dialog (which
  writes the config directly) updates the transaction list immediately instead
  of requiring an Electrum restart.

UI - bold results in the Building Will dialog:
- gui/qt/dialogs.py (BalBuildWillDialog): render the right-side results in bold
  (Ok, Ko, Nothing to do, Skipped, Wait, Timeout, ...) keeping the left-side
  state labels in normal weight. Centralised in msg_ok/msg_error/msg_warning/
  msg_set_status, plus the will-executor push/check rows now show Ok/Ko and
  True/False in bold + colour (green/red).

Tests/tooling:
- tests/test_core_will.py: add test_check_heirs_unchanged_is_coherent,
  test_check_heir_removed_triggers_rebuild, test_check_heir_added_triggers_rebuild,
  test_needs_server_check.
- tests/sim_update_flows.py: real-world update-scenario simulation.
- tests/preview_build_will_dialog.py, tests/preview_we_rows.py: GUI-only
  before/after previews of the bold formatting.
- Bump version to 0.3.2 (VERSION, manifest.json, __init__.py, plugin_base.py).

186 tests pass; smoke test, external-zip test and update-flow simulation OK;
ruff reports only pre-existing star-import false positives.
2026-06-28 22:51:28 -04:00

172 lines
6.6 KiB
Python

"""
Real-world simulation of the inheritance update flows.
This script does NOT touch the GUI. It drives the core decision function
``Will.check_willexecutors_and_heirs`` (the one that decides whether a will is
still coherent or must be rebuilt) through the scenarios the user reported:
1. delivery date moved forward (postpone) -> must NOT stay "coherent"
2. an heir is added -> must trigger rebuild
3. an heir is removed -> must trigger rebuild
4. a single heir percentage / amount is changed -> must trigger rebuild
5. nothing changed -> stays coherent
For each scenario we report which exception (if any) is raised, because that is
exactly what the GUI relies on to decide whether to rebuild the inheritance
transactions. If the function returns True (coherent) when something DID
change, the GUI will (correctly) show no update -- which is the symptom the
user described.
Run:
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 tests/sim_update_flows.py
"""
import sys
import os
import copy
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.will import (
WillItem, Will,
NotCompleteWillException, HeirNotFoundException, NoHeirsException,
TxFeesChangedException, WillExpiredException,
)
from bal.core.util import Util
# A valid serialized tx (1 input + 1 output, version 2). Its nLockTime is 0.
_VALID_TX_HEX = (
"01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b"
"f38633b424eb4031000000006c493046022100a82bbc57a0136751e543"
"3f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7d"
"e89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501"
"2102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae3"
"5cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a"
"42146f11ef8414ae929feaafc388ac00000000"
)
# A locktime far in the past (so the frozen tx.locktime is a fixed integer we
# control via monkey-patching below). We will override w.tx.locktime per test.
TX_FEES = 100
def _make_will_item(heirs, tx_locktime, status_complete=False):
"""Build a WillItem whose stored heirs == ``heirs`` and whose tx.locktime
is forced to ``tx_locktime`` (the value frozen in the signed Bitcoin tx)."""
d = {
"tx": _VALID_TX_HEX,
"heirs": copy.deepcopy(heirs),
"willexecutor": None,
"status": "",
"description": "",
"time": 0,
"change": "",
"baltx_fees": TX_FEES,
}
item = WillItem(d, _id="willid_1")
item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
# Force the locktime frozen "inside" the signed tx.
item.tx.locktime = tx_locktime
if status_complete:
item.set_status("COMPLETE", True)
return item
def _run(label, will_heirs, current_heirs, tx_locktime,
status_complete=False, check_date=0):
"""Run check_willexecutors_and_heirs and report the outcome."""
item = _make_will_item(will_heirs, tx_locktime, status_complete)
will = {"willid_1": item}
outcome = None
try:
result = Will.check_willexecutors_and_heirs(
will,
current_heirs, # the (possibly edited) heirs dict
{}, # willexecutors
False, # self_willexecutor
check_date, # check_date (timestamp)
TX_FEES, # tx_fees
)
outcome = f"coherent (returned {result})"
except HeirNotFoundException as e:
outcome = f"HeirNotFoundException: {e}"
except NoHeirsException as e:
outcome = f"NoHeirsException: {e}"
except TxFeesChangedException as e:
outcome = f"TxFeesChangedException: {e}"
except WillExpiredException as e:
outcome = f"WillExpiredException: {e}"
except NotCompleteWillException as e:
outcome = f"{type(e).__name__}: {e}"
except Exception as e:
outcome = f"!! UNEXPECTED {type(e).__name__}: {e}"
print(f"[{label}]")
print(f" -> {outcome}")
return outcome
def main():
# locktime string "0d" -> Util.parse_locktime_string returns a timestamp
# ~ now. We use explicit integer timestamps to keep things deterministic.
base_lt = 1900000000 # frozen tx.locktime (year ~2030)
later_lt = "2000000000" # a later locktime string (postpone)
same_lt = str(base_lt)
# Scenario 0: nothing changed -> should be coherent.
heirs = {"alice": ["addr_alice", 5000, same_lt]}
_run("0. nothing changed",
will_heirs=heirs, current_heirs=copy.deepcopy(heirs),
tx_locktime=base_lt, check_date=0)
# Scenario 1: delivery date moved forward (postpone), will NOT yet signed.
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
heirs_now = {"alice": ["addr_alice", 5000, later_lt]}
_run("1. date postponed (unsigned will)",
will_heirs=heirs_will, current_heirs=heirs_now,
tx_locktime=base_lt, check_date=0)
# Scenario 1b: postpone on a SIGNED will (status COMPLETE).
_run("1b. date postponed (SIGNED will)",
will_heirs=heirs_will, current_heirs=heirs_now,
tx_locktime=base_lt, status_complete=True, check_date=0)
# Scenario 2: an heir is ADDED.
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
heirs_now = {
"alice": ["addr_alice", 5000, same_lt],
"bob": ["addr_bob", 3000, same_lt],
}
_run("2. heir added (bob)",
will_heirs=heirs_will, current_heirs=heirs_now,
tx_locktime=base_lt, check_date=0)
# Scenario 3: an heir is REMOVED.
heirs_will = {
"alice": ["addr_alice", 5000, same_lt],
"bob": ["addr_bob", 3000, same_lt],
}
heirs_now = {"alice": ["addr_alice", 5000, same_lt]}
_run("3. heir removed (bob)",
will_heirs=heirs_will, current_heirs=heirs_now,
tx_locktime=base_lt, check_date=0)
# Scenario 4: a single heir AMOUNT/percentage changed.
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
heirs_now = {"alice": ["addr_alice", 9999, same_lt]}
_run("4. heir amount changed (5000 -> 9999)",
will_heirs=heirs_will, current_heirs=heirs_now,
tx_locktime=base_lt, check_date=0)
# Scenario 5: heir ADDRESS changed.
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
heirs_now = {"alice": ["addr_NEW", 5000, same_lt]}
_run("5. heir address changed",
will_heirs=heirs_will, current_heirs=heirs_now,
tx_locktime=base_lt, check_date=0)
print("\n[done] simulation finished")
if __name__ == "__main__":
main()