feat: VOTE ULANG SEGERA Telegram notification + emoticon on all notifications

- telegram.py: emoticons on all existing notif (🚀✅⚠️🔴💰🔍) + new notify_warn for expiring voters
- db.py: voter_notify table (owner/urgency/notified_at) for state-change dedup
- get_voters.py: _notify_warn() after persist() — urgency escalation ⚠️/🔴/🚨 by days remaining
- Dockerfile.scan: add telegram.py to COPY
- test_get_voters.py: 15 new V66 assertions (schema, record, cleanup, integration, escalation)
- test_distribute.py: updated notification format assertions
- SPEC.md: V66 + B26
This commit is contained in:
proitlab committed 2026-08-27 16:12:53 +07:00
1 parent eaefad6c1d
commit 1e93437a90
7 files changed
+345 -31

No files matched your search

+110
View File
@@ -16,6 +16,7 @@ import requests
import db
import get_voters as gv
import telegram
def _fresh_weight(staked_raw, days_ago=5):
@@ -257,6 +258,115 @@ def main():
check('V41 first_seen idempoten (record ulang ⊥ overwrite)',
fs['aaa1'] == scanned_a and fs['aaa1'] != '2000-01-01T00:00:00')
# V66: voter_notify — tabel lacak notifikasi VOTE ULANG SEGERA
db.ensure_notify_schema()
conn = sqlite3.connect(db.DB_PATH)
cols = [r[1] for r in conn.execute(
'PRAGMA table_info(voter_notify)').fetchall()]
pk = [r[1] for r in conn.execute(
'PRAGMA table_info(voter_notify)').fetchall() if r[5]]
conn.close()
check('V66 voter_notify tabel ada', 'owner' in cols)
check('V66 voter_notify PK = owner', pk == ['owner'])
check('V66 urgency kolom ada', 'urgency' in cols)
check('V66 notified_at kolom ada', 'notified_at' in cols)
# record_notify: insert baru
db.record_notify('aaa1', 1, '2026-08-27T08:00:00')
check('V66 get_notify_urgency(aaa1) = 1', db.get_notify_urgency('aaa1') == 1)
check('V66 get_notify_urgency(unk) = None', db.get_notify_urgency('unk9') is None)
# record_notify: idempoten (INSERT OR IGNORE) — urgency tetap 1
db.record_notify('aaa1', 1, '2026-08-27T08:00:00')
check('V66 record_notify idempoten', db.get_notify_urgency('aaa1') == 1)
# record_notify: urgency naik → update
db.record_notify('aaa1', 2, '2026-08-28T08:00:00')
check('V66 urgency naik 1→2', db.get_notify_urgency('aaa1') == 2)
# urgency turun → ⊥ update
db.record_notify('aaa1', 1, '2026-08-29T08:00:00')
check('V66 urgency turun 2→1 ⊥ update', db.get_notify_urgency('aaa1') == 2)
# cleanup_notify: hapus baris tua
db.record_notify('old1', 1, '2026-08-01T00:00:00')
db.record_notify('new1', 1, '2026-08-27T00:00:00')
db.cleanup_notify('2026-08-15T00:00:00')
check('V66 cleanup hapus baris tua', db.get_notify_urgency('old1') is None)
check('V66 cleanup simpan baris baru', db.get_notify_urgency('new1') == 1)
# V66: _notify_warn — integrasi dengan telegram.notify_warn
# Siapkan pemilih dalam jendela VOTE ULANG SEGERA (last_vote 26 hari lalu)
warn_now = datetime(2026, 8, 27, 8, 0, 0)
warn_vote = (warn_now - timedelta(days=26)).isoformat(timespec='seconds')
safe_vote = (warn_now - timedelta(days=20)).isoformat(timespec='seconds')
# Masukkan pemilih langsung ke DB (bypass scan)
wrn2_vote = (warn_now - timedelta(days=27)).isoformat(timespec='seconds')
db.replace_snapshot([
{'owner': 'wrn1', 'weight': '1.0', 'staked': 5000.0,
'last_vote': warn_vote},
{'owner': 'wrn2', 'weight': '2.0', 'staked': 3000.0,
'last_vote': wrn2_vote},
{'owner': 'safe', 'weight': '3.0', 'staked': 8000.0,
'last_vote': safe_vote},
], warn_now.isoformat(timespec='seconds'))
sent = []
_orig_warn = telegram.notify_warn
telegram.notify_warn = lambda v: sent.append(v)
try:
gv._notify_warn(warn_now)
check('V66 _notify_warn dipanggil', len(sent) == 1)
voters_notified = sent[0]
owners_notified = [v[0] for v in voters_notified]
check('V66 wrn2 (27h, ≤2d) masuk', 'wrn2' in owners_notified)
check('V66 wrn1 (26h, >2d) masuk', 'wrn1' in owners_notified)
check('V66 safe (20h, ⊥ window) tak masuk', 'safe' not in owners_notified)
# Cek urgensi
by_owner = {v[0]: v[2] for v in voters_notified}
check('V66 wrn2 urgensi = 3 (≤1d)', by_owner['wrn2'] == 3)
check('V66 wrn1 urgensi = 2 (≤2d)', by_owner['wrn1'] == 2)
# Cek voter_notify tercatat
check('V66 voter_notify wrn2 = 3', db.get_notify_urgency('wrn2') == 3)
check('V66 voter_notify wrn1 = 2', db.get_notify_urgency('wrn1') == 2)
finally:
telegram.notify_warn = _orig_warn
# V66: _notify_warn — skip bila sudah diberi tahu pada urgensi ≥ saat ini
sent.clear()
telegram.notify_warn = lambda v: sent.append(v)
try:
gv._notify_warn(warn_now) # wrn1 sudah urgensi 2, wrn2 sudah urgensi 3
check('V66 _notify_warn skip redundancy', len(sent) == 0)
finally:
telegram.notify_warn = _orig_warn
# V66: _notify_warn — escalation trigger (wrn1 dari 2→3)
sent.clear()
# Hapus lacak utk wrn1 supaya bisa test escalation dratch
conn = sqlite3.connect(db.DB_PATH)
conn.execute('DELETE FROM voter_notify WHERE owner = ?', ('wrn1',))
conn.commit()
conn.close()
# Update wrn1 last_vote ke 27 hari lalu (≤1d → urgensi 3)
closer_vote = (warn_now - timedelta(days=27)).isoformat(
timespec='seconds')
db.replace_snapshot([
{'owner': 'wrn1', 'weight': '1.0', 'staked': 5000.0,
'last_vote': closer_vote},
{'owner': 'wrn2', 'weight': '2.0', 'staked': 3000.0,
'last_vote': wrn2_vote},
], warn_now.isoformat(timespec='seconds'))
telegram.notify_warn = lambda v: sent.append(v)
try:
gv._notify_warn(warn_now)
check('V66 escalation trigger (2→3)', len(sent) == 1)
by_owner = {v[0]: v[2] for v in sent[0]}
check('V66 wrn1 escalation ke urgensi 3', by_owner['wrn1'] == 3)
check('V66 voter_notify wrn1 updated ke 3',
db.get_notify_urgency('wrn1') == 3)
finally:
telegram.notify_warn = _orig_warn
print('\nSEMUA UJI PASS')