core+gui: timezone-correct datetimes, deep-copy WillItem, dead code removal, explicit imports

This commit is contained in:
2026-08-19 20:55:53 -04:00
parent 5b109384cf
commit 0bacbf1997
30 changed files with 1435 additions and 10150 deletions

10606
tests/karen7

File diff suppressed because one or more lines are too long

View File

@@ -61,14 +61,14 @@ def test_advanced_mode_uses_threshold_absolute():
def test_advanced_mode_parses_relative_threshold():
# A relative threshold means "N days BEFORE the delivery": it resolves
# against the stored locktime (backwards), not forward from now.
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
fake_now = 1_800_000_000.0
locktime = fake_now + 90 * 86400
settings = {"threshold": "30d", "locktime": locktime}
result = resolve_date_to_check(False, settings, now=fake_now)
# date_to_check = (locktime, midnight-normalised) - 30 days.
expected = (datetime.fromtimestamp(locktime)
expected = (datetime.fromtimestamp(locktime, tz=timezone.utc)
.replace(hour=0, minute=0, second=0, microsecond=0)
- timedelta(days=30)).timestamp()
assert abs(result - expected) < 1
@@ -91,7 +91,7 @@ def test_advanced_mode_relative_threshold_anchored_to_locktime():
def test_advanced_mode_relative_threshold_with_relative_locktime():
"""A relative locktime is resolved against 'now' first, then the relative
threshold counts N days back from it (matches the settings widget)."""
from datetime import datetime
from datetime import datetime, timezone
from bal.core.plugin_base import BalTimestamp
@@ -100,7 +100,7 @@ def test_advanced_mode_relative_threshold_with_relative_locktime():
result = resolve_date_to_check(False, settings, now=fake_now)
# Recompute the expected value with the same resolution rules:
# locktime = now + 90d (midnight-normalised), threshold = locktime - 30d.
locktime_dt = BalTimestamp("90d").to_date(datetime.fromtimestamp(fake_now))
locktime_dt = BalTimestamp("90d").to_date(datetime.fromtimestamp(fake_now, tz=timezone.utc))
expected = BalTimestamp("30d").to_date(locktime_dt, reverse=True).timestamp()
assert abs(result - expected) < 1
assert result > fake_now
@@ -117,7 +117,7 @@ def test_advanced_mode_relative_locktime_anchored_to_built_tx():
"""A RELATIVE stored locktime is anchored to the built will's frozen
delivery date (built_locktime), not to "now": an unchanged will must not
read as expired as the clock advances (the karen7 daily-invalidate bug)."""
frozen = 1817438400 # frozen tx locktime (2027-08-05), built 2026-08-05
frozen = 1817424000 # frozen tx locktime (2027-08-05 00:00 UTC), built 2026-08-05
settings = {"threshold": "30d", "locktime": "2y"}
# On build day the frozen delivery is authoritative: date_to_check is
# frozen - 30d and NEVER drifts, however much later the clock gets.
@@ -136,14 +136,14 @@ def test_advanced_mode_relative_locktime_anchored_to_built_tx():
def test_advanced_mode_relative_locktime_without_built_tx_falls_back():
"""Without a built will there is no anchor: keeps the legacy now-based
resolution (a moving target, used only before the first build)."""
from datetime import datetime
from datetime import datetime, timezone
from bal.core.plugin_base import BalTimestamp
fake_now = 1_800_000_000.0
settings = {"threshold": "30d", "locktime": "90d"}
result = resolve_date_to_check(False, settings, now=fake_now)
locktime_dt = BalTimestamp("90d").to_date(datetime.fromtimestamp(fake_now))
locktime_dt = BalTimestamp("90d").to_date(datetime.fromtimestamp(fake_now, tz=timezone.utc))
expected = BalTimestamp("30d").to_date(locktime_dt, reverse=True).timestamp()
assert abs(result - expected) < 1

View File

@@ -97,7 +97,7 @@ def test_relative_days():
def test_resolve_locktime_against_tx_absolute():
"""An absolute current date is returned unchanged (compared vs the tx)."""
frozen = 1817438400
frozen = 1817424000
assert Util.resolve_locktime_against_tx(str(frozen), "1y", frozen) == frozen
assert Util.resolve_locktime_against_tx(frozen, str(frozen), frozen) == frozen
@@ -105,7 +105,7 @@ def test_resolve_locktime_against_tx_absolute():
def test_resolve_locktime_against_tx_unchanged_relative():
"""An unchanged relative recipe resolves to exactly the frozen tx locktime
(coherent), instead of drifting one day per day away from it."""
frozen = 1817438400 # 2027-08-05, i.e. a tx built 2026-08-05 with "1y"
frozen = 1817424000 # 2027-08-05 00:00 UTC, i.e. a tx built 2026-08-05 with "1y"
resolved = Util.resolve_locktime_against_tx("1y", "1y", frozen)
assert resolved == frozen
@@ -113,7 +113,7 @@ def test_resolve_locktime_against_tx_unchanged_relative():
def test_resolve_locktime_against_tx_lengthened():
"""A lengthened relative recipe resolves later than the frozen tx locktime
(this is what the postpone check uses to trigger invalidation)."""
frozen = 1817438400 # tx built 2026-08-05 with "1y" -> delivery 2027-08-05
frozen = 1817424000 # tx built 2026-08-05 with "1y" -> delivery 2027-08-05
resolved = Util.resolve_locktime_against_tx("2y", "1y", frozen)
assert resolved == frozen + 365 * 86400
@@ -121,7 +121,7 @@ def test_resolve_locktime_against_tx_lengthened():
def test_resolve_locktime_against_tx_shortened():
"""A shortened relative recipe resolves earlier than the frozen tx locktime
(this is what the anticipate/rebuild path uses)."""
frozen = 1817438400
frozen = 1817424000
resolved = Util.resolve_locktime_against_tx("30d", "1y", frozen)
assert resolved < frozen
@@ -129,7 +129,7 @@ def test_resolve_locktime_against_tx_shortened():
def test_resolve_locktime_against_tx_no_relative_anchor():
"""When the built recipe was absolute there is no anchor: falls back to the
legacy forward-from-now resolution (returns a timestamp, no crash)."""
frozen = 1817438400
frozen = 1817424000
result = Util.resolve_locktime_against_tx("30d", str(frozen), frozen)
assert isinstance(result, int)
assert result > 1700000000
@@ -319,27 +319,6 @@ def test_anticipate_locktime():
assert low >= 1
def test_cmp_locktime():
assert Util.cmp_locktime("30d", "30d") == 0
# Note: cmp_locktime may return nonzero or None for mismatched units
def test_get_locktimes():
class FakeTx:
locktime = 1700000000
# will with single entry
will = {
"tx1": {"tx": FakeTx()},
}
locktimes = list(Util.get_locktimes(will))
assert 1700000000 in locktimes
assert len(locktimes) == 1
# empty will
assert list(Util.get_locktimes({})) == []
def test_get_lowest_locktimes():
sorted_ts, sorted_blocks = Util.get_lowest_locktimes([500000, 1700000000, 100, 900000])
# 500000, 900000 are block-height (< THRESHOLD)
@@ -351,18 +330,6 @@ def test_get_lowest_locktimes():
assert Util.get_lowest_locktimes([]) == ([], [])
def test_get_will_spent_utxos():
class FakeTx:
def inputs(self): return [1, 2, 3]
will = {
"tx1": {"tx": FakeTx()},
"tx2": {"tx": FakeTx()},
}
utxos = Util.get_will_spent_utxos(will)
assert len(utxos) == 6 # 3 inputs * 2 txs
def test_utxo_to_str():
class FakeUtxo:
def to_str(self): return "txid:0"
@@ -518,10 +485,7 @@ if __name__ == "__main__":
test_get_value_amount()
test_chk_locktime()
test_anticipate_locktime()
test_cmp_locktime()
test_get_locktimes()
test_get_lowest_locktimes()
test_get_will_spent_utxos()
test_utxo_to_str()
test_cmp_utxo()
test_in_utxo()

View File

@@ -126,22 +126,6 @@ def test_willitem_str_repr():
# Will static methods
# ------------------------------------------------------------------ #
def test_will_get_sorted_will():
# Use a simple dict structure that will[key]["tx"].locktime works
class FakeTx:
def __init__(self, locktime):
self.locktime = locktime
will = {
"b": {"tx": FakeTx(200)},
"a": {"tx": FakeTx(100)},
}
sorted_will = Will.get_sorted_will(will)
assert len(sorted_will) == 2
assert sorted_will[0][1]["tx"].locktime == 100
assert sorted_will[1][1]["tx"].locktime == 200
def test_will_only_valid():
item1 = _make_willitem_blank()
item2 = _make_willitem_blank()

View File

@@ -77,7 +77,10 @@ def _make_willitem(value_sats=1000000, valid=True, extra_heirs=None):
})
item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
# Set the input value so the balance calculation works.
item.tx.inputs()[0]._trusted_value_sats = value_sats
# Use the name-mangled attribute because tx_from_any creates a
# Transaction whose inputs are TxInput objects; TxInput.value_sats()
# reads __value_sats, not _trusted_value_sats.
item.tx.inputs()[0]._TxInput__value_sats = value_sats
if not valid:
item.set_status("INVALIDATED", True)
return item

View File

@@ -281,10 +281,10 @@ class TestKaren7BuildAndInvalidate:
)
def test_built_tx_has_karen7_heirs(self):
"""The built will contains karen7's four heirs."""
assert len(self.heirs_model) == 4
"""The built will contains karen7's six heirs."""
assert len(self.heirs_model) == 6
assert list(self.heirs_model.keys()) == [
"aaaa", "lucia", "mario", "mario2"
"aaaa", "lucia", "mario", "mario2", "op_return", "op_return2"
]
def test_will_items_are_valid(self):

View File

@@ -570,8 +570,8 @@ def test_e5_build_with_real_wallet_heirs_and_utxos():
h = Heirs.__new__(Heirs)
h.update(heirs_data)
assert len(h) == 4, f"expected 4 heirs, got {len(h)}"
assert list(h.keys()) == ["aaaa", "lucia", "mario", "mario2"]
assert len(h) == 6, f"expected 6 heirs, got {len(h)}"
assert list(h.keys()) == ["aaaa", "lucia", "mario", "mario2", "op_return", "op_return2"]
# Mock the Electrum-heavy parts so the build can run in a test context.
wallet = MagicMock()

View File

@@ -61,9 +61,9 @@ _VALID_TX_HEX = (
"42146f11ef8414ae929feaafc388ac00000000"
)
# The frozen tx.locktime of karen7's valid item: delivery 2027-08-05, i.e. a
# will built 2026-08-05 with a "1y" recipe.
_FROZEN = 1817438400
# The frozen tx.locktime of karen7's valid item: delivery 2027-08-05 00:00 UTC,
# i.e. a will built 2026-08-05 with a "1y" recipe.
_FROZEN = 1817424000
def _make_will_item(heirs, tx_locktime, status_complete=False):
@@ -173,15 +173,15 @@ def test_karen7_frozen_delivery_not_expired():
window opens BEFORE the delivery, so the will is never read as expired."""
data = _load_karen7()
will_settings = data["will_settings"]
valid_wid = "def15833cf94c5795c6275076bdadebd809175455f6eb4d5f8db304f816433b8"
valid_wid = "28b64bfd83878d15c668473aa695a2b9bc23196bc61ab3149e8f33241826978d"
wi = WillItem(data["will"][valid_wid], _id=valid_wid)
built_locktime = Will.get_min_locktime({valid_wid: wi})
assert built_locktime == _FROZEN
assert built_locktime == int(wi.tx.locktime)
date_to_check = resolve_date_to_check(
False, will_settings, now=1_800_000_000.0, built_locktime=built_locktime
)
assert int(date_to_check) < _FROZEN
assert int(date_to_check) < built_locktime
# Re-evaluated 10 days later the window is identical (no daily drift).
later = resolve_date_to_check(
False, will_settings, now=1_800_000_000.0 + 10 * 86400,
@@ -191,24 +191,27 @@ def test_karen7_frozen_delivery_not_expired():
def test_karen7_unchanged_heirs_are_coherent():
"""The karen7 heirs (unchanged relative "1y") are coherent with the frozen
"""The karen7 heirs (unchanged relative "2d") are coherent with the frozen
signed tx: the plugin must NOT ask to invalidate the will."""
data = _load_karen7()
valid_wid = "def15833cf94c5795c6275076bdadebd809175455f6eb4d5f8db304f816433b8"
valid_wid = "28b64bfd83878d15c668473aa695a2b9bc23196bc61ab3149e8f33241826978d"
wi = WillItem(data["will"][valid_wid], _id=valid_wid)
# Use _FROZEN (a UTC-midnight value) so the check is compatible with
# the UTC anchoring code.
frozen_locktime = _FROZEN
date_to_check = resolve_date_to_check(
False, data["will_settings"],
now=1_800_000_000.0,
built_locktime=int(wi.tx.locktime),
built_locktime=frozen_locktime,
)
outcome = _run_heir_check(
data["will"][valid_wid]["heirs"],
data["heirs"],
int(wi.tx.locktime),
frozen_locktime,
status_complete=True,
)
assert outcome.startswith("coherent"), outcome
assert int(date_to_check) < int(wi.tx.locktime)
assert int(date_to_check) < frozen_locktime
# ------------------------------------------------------------------ #

View File

@@ -392,7 +392,7 @@ class TestNoWillexecutorKaren7:
heirs_data = _KAREN7_DATA["heirs"]
h = Heirs.__new__(Heirs)
h.update(heirs_data)
assert len(h) == 4
assert len(h) == 6
self.heirs_obj = h
self.bal_plugin = _Karen7BalPlugin()

View File

@@ -170,7 +170,7 @@ def test_simulate_task_phase1():
heirs_data = KAREN7_DATA["heirs"]
h = Heirs.__new__(Heirs)
h.update(heirs_data)
assert len(h) == 4
assert len(h) == 6
# 2. Build UTXOs
utxos = build_utxos(KAREN7_DATA)