From 20ab7eaa15c067533c6aef6456fc2dfbcbd32c00 Mon Sep 17 00:00:00 2001 From: ScuttoZ Date: Thu, 26 Mar 2026 15:48:22 +0100 Subject: [PATCH 1/6] bkpr: test added for {utctime} tag --- tests/test_bookkeeper.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_bookkeeper.py b/tests/test_bookkeeper.py index c62210e6b780..06ce4f92bafd 100644 --- a/tests/test_bookkeeper.py +++ b/tests/test_bookkeeper.py @@ -1347,3 +1347,32 @@ def test_bkpr_report_lightning_cli_csv(node_factory): parsed = [next(csv.reader(io.StringIO(line))) for line in res.splitlines()] assert parsed assert all(len(row) == 3 for row in parsed) + + +def test_bkpr_report_utctime(node_factory): + """Test {utctime} format tag. + + {utctime} is the UTC counterpart of {localtime}. Verify it produces valid + "YYYY-MM-DD HH:MM:SS" strings and that its tag column matches {localtime}. + """ + l1, l2 = node_factory.line_graph(2) + + inv = l2.rpc.invoice(100000, "test_bkpr_report_utctime", "desc") + l1.rpc.pay(inv["bolt11"]) + wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['htlcs'] == []) + + utc_lines = l1.rpc.bkpr_report(format="{utctime},{tag}")['report'] + loc_lines = l1.rpc.bkpr_report(format="{localtime},{tag}")['report'] + + assert utc_lines + assert len(utc_lines) == len(loc_lines) + + for u, lc in zip(utc_lines, loc_lines): + u_ts_str, u_tag = u.split(',') + l_ts_str, l_tag = lc.split(',') + # Both must produce valid "YYYY-MM-DD HH:MM:SS" strings. + datetime.strptime(u_ts_str, "%Y-%m-%d %H:%M:%S") + datetime.strptime(l_ts_str, "%Y-%m-%d %H:%M:%S") + # Both describe the same event. + assert u_tag == l_tag + From 5e2a7fe0ffe85468f6db7e6ee8bdb107f725aebc Mon Sep 17 00:00:00 2001 From: ScuttoZ Date: Thu, 26 Mar 2026 17:28:09 +0100 Subject: [PATCH 2/6] bkpr: test utctime, unified timestamp fetching to avoid races --- tests/test_bookkeeper.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/test_bookkeeper.py b/tests/test_bookkeeper.py index 06ce4f92bafd..f0b469ead009 100644 --- a/tests/test_bookkeeper.py +++ b/tests/test_bookkeeper.py @@ -1361,18 +1361,13 @@ def test_bkpr_report_utctime(node_factory): l1.rpc.pay(inv["bolt11"]) wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['htlcs'] == []) - utc_lines = l1.rpc.bkpr_report(format="{utctime},{tag}")['report'] - loc_lines = l1.rpc.bkpr_report(format="{localtime},{tag}")['report'] + # Fetch both timestamps in a single call to avoid a race between two + # separate bkpr-report calls where a background event could land in between. + lines = l1.rpc.bkpr_report(format="{utctime}|{localtime}|{tag}")['report'] - assert utc_lines - assert len(utc_lines) == len(loc_lines) - - for u, lc in zip(utc_lines, loc_lines): - u_ts_str, u_tag = u.split(',') - l_ts_str, l_tag = lc.split(',') + assert lines + for line in lines: + u_ts_str, l_ts_str, tag = line.split('|') # Both must produce valid "YYYY-MM-DD HH:MM:SS" strings. datetime.strptime(u_ts_str, "%Y-%m-%d %H:%M:%S") datetime.strptime(l_ts_str, "%Y-%m-%d %H:%M:%S") - # Both describe the same event. - assert u_tag == l_tag - From b3208605ad5864089f626f0e1b77b99bd1d01d26 Mon Sep 17 00:00:00 2001 From: ScuttoZ Date: Thu, 26 Mar 2026 17:57:44 +0100 Subject: [PATCH 3/6] bkpr: test added for {fees} tag --- tests/test_bookkeeper.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_bookkeeper.py b/tests/test_bookkeeper.py index f0b469ead009..8925eaca37ae 100644 --- a/tests/test_bookkeeper.py +++ b/tests/test_bookkeeper.py @@ -1371,3 +1371,30 @@ def test_bkpr_report_utctime(node_factory): # Both must produce valid "YYYY-MM-DD HH:MM:SS" strings. datetime.strptime(u_ts_str, "%Y-%m-%d %H:%M:%S") datetime.strptime(l_ts_str, "%Y-%m-%d %H:%M:%S") + + +def test_bkpr_report_fees(node_factory): + """Test {fees} format tag. + + {fees} is non-zero only when routing fees are incurred. A 3-node path + (l1 -> l2 -> l3) ensures l1's income events carry non-zero routing fees. + """ + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + inv = l3.rpc.invoice(100000, "test_bkpr_report_fees", "desc") + l1.rpc.pay(inv["bolt11"]) + wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['htlcs'] == []) + + lines = l1.rpc.bkpr_report(format="{tag},{fees}")['report'] + assert lines + + # Every row must produce a parseable non-negative decimal. + for line in lines: + tag, fees_str = line.split(',') + assert float(fees_str) >= 0 + + # This type of payment should produce exactly 2 non-zero fee events. + nonzero = [line for line in lines if float(line.split(',')[1]) > 0] + assert len(nonzero) == 2 + tags = {line.split(',')[0] for line in nonzero} + assert tags == {'invoice', 'invoice_fee'} From 393a84b7de573bb07d420295ebb4da8e412110fd Mon Sep 17 00:00:00 2001 From: ScuttoZ Date: Thu, 26 Mar 2026 17:59:27 +0100 Subject: [PATCH 4/6] bkpr: test added for NULL currency format tag fallback --- tests/test_bookkeeper.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_bookkeeper.py b/tests/test_bookkeeper.py index 8925eaca37ae..de39d7616a3d 100644 --- a/tests/test_bookkeeper.py +++ b/tests/test_bookkeeper.py @@ -1398,3 +1398,31 @@ def test_bkpr_report_fees(node_factory): assert len(nonzero) == 2 tags = {line.split(',')[0] for line in nonzero} assert tags == {'invoice', 'invoice_fee'} + + +def test_bkpr_report_no_currency(node_factory): + """All currency-related format tags must all resolve to NULL + and trigger their fallback text.""" + l1, l2 = node_factory.line_graph(2) + + inv = l2.rpc.invoice(100000, "test_bkpr_report_no_currency", "desc") + l1.rpc.pay(inv["bolt11"]) + wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['htlcs'] == []) + + fmt = ("{tag}" + "|{bkpr-currency?NOCUR}" + "|{currencyrate?NORAT}" + "|{currencycredit?NOCREDIT}" + "|{currencydebit?NODEBIT}" + "|{currencycreditdebit?NOCD}") + lines = l1.rpc.bkpr_report(format=fmt)['report'] + assert lines + + for line in lines: + parts = line.split('|') + assert len(parts) == 6 + assert parts[1] == 'NOCUR' + assert parts[2] == 'NORAT' + assert parts[3] == 'NOCREDIT' + assert parts[4] == 'NODEBIT' + assert parts[5] == 'NOCD' From b028cf04be7d6c4097de471f2f9e04921d85775a Mon Sep 17 00:00:00 2001 From: ScuttoZ Date: Thu, 26 Mar 2026 18:02:04 +0100 Subject: [PATCH 5/6] bkpr: test added for escape=none report mode --- tests/test_bookkeeper.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_bookkeeper.py b/tests/test_bookkeeper.py index de39d7616a3d..5c12c221de58 100644 --- a/tests/test_bookkeeper.py +++ b/tests/test_bookkeeper.py @@ -1426,3 +1426,34 @@ def test_bkpr_report_no_currency(node_factory): assert parts[3] == 'NOCREDIT' assert parts[4] == 'NODEBIT' assert parts[5] == 'NOCD' + + +def test_bkpr_report_escape_none(node_factory): + """escape=none must leave special characters unescaped in the output, + in contrast to escape=csv which wraps fields containing commas/quotes.""" + l1, l2 = node_factory.line_graph(2) + + # Description with a comma so CSV-sensitive escaping is detectable. + inv = l2.rpc.invoice(100000, "test_bkpr_report_escape_none", 'hello, world') + l1.rpc.pay(inv["bolt11"]) + wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['htlcs'] == []) + + # escape=none (explicit): description must appear verbatim with its comma. + lines_none = l1.rpc.bkpr_report(format="{description?-},{tag}", + escape='none')['report'] + # escape=csv: description containing a comma must be quoted. + lines_csv = l1.rpc.bkpr_report(format="{description?-},{tag}", + escape='csv')['report'] + + assert len(lines_none) == len(lines_csv) + + # Find the invoice row — it has the description with the embedded comma. + inv_none = only_one([l for l in lines_none if 'invoice' in l.split(',')[-1]]) + inv_csv = only_one([l for l in lines_csv if 'invoice' in l.split(',')[-1]]) + + # With escape=none the comma in the description is NOT escaped. + assert 'hello, world' in inv_none + # With escape=csv the description field is quoted, so csv.reader collapses + # it back to a single field containing the original string. + parsed = next(csv.reader(io.StringIO(inv_csv))) + assert parsed[0] == 'hello, world' From c08c1b250d66a985e7d1783cd07f47de07e58113 Mon Sep 17 00:00:00 2001 From: ScuttoZ Date: Thu, 26 Mar 2026 18:05:27 +0100 Subject: [PATCH 6/6] bkpr: test added for reports with future starting date (empty) and linted --- tests/test_bookkeeper.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_bookkeeper.py b/tests/test_bookkeeper.py index 5c12c221de58..0bac47cb4837 100644 --- a/tests/test_bookkeeper.py +++ b/tests/test_bookkeeper.py @@ -1439,11 +1439,11 @@ def test_bkpr_report_escape_none(node_factory): wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['htlcs'] == []) # escape=none (explicit): description must appear verbatim with its comma. - lines_none = l1.rpc.bkpr_report(format="{description?-},{tag}", - escape='none')['report'] + lines_none = l1.rpc.bkpr_report( + format="{description?-},{tag}", escape='none')['report'] # escape=csv: description containing a comma must be quoted. - lines_csv = l1.rpc.bkpr_report(format="{description?-},{tag}", - escape='csv')['report'] + lines_csv = l1.rpc.bkpr_report( + format="{description?-},{tag}", escape='csv')['report'] assert len(lines_none) == len(lines_csv) @@ -1457,3 +1457,19 @@ def test_bkpr_report_escape_none(node_factory): # it back to a single field containing the original string. parsed = next(csv.reader(io.StringIO(inv_csv))) assert parsed[0] == 'hello, world' + + +def test_bkpr_report_empty_window(node_factory, bitcoind): + """bkpr-report with a start_time beyond all events must return an empty + list without errors.""" + l1 = node_factory.get_node() + addr = l1.rpc.newaddr()['p2tr'] + + bitcoind.rpc.sendtoaddress(addr, 0.01) + bitcoind.generate_block(1, wait_for_mempool=1) + wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1) + + future = int(time.time()) + 10_000_000 + report = l1.rpc.bkpr_report( + format="{tag},{creditdebit}", start_time=future)['report'] + assert report == []