Skip to content

Commit d961644

Browse files
jkczyzclaude
andcommitted
Test splice failure surfacing and recovery
A disconnect during the interactive negotiation fails the splice with PeerDisconnected. The first test asserts that exactly one SpliceNegotiationFailed reaches the user — carrying the reason and the originating request's parameters — and that a new splice initiated afterwards completes with a single funding payment. The window only exists mid-negotiation: a contribution still queued at disconnect is resumed by LDK itself on reconnect, and one awaiting signatures survives re-establishment. The test therefore synchronizes on the counterparty's splice_ack — logged by LDK's peer handler — and stretches the negotiation by funding the splice from many small UTXOs, each of which adds an interactive-tx round trip. A splice dropped by a restart is recovered silently: startup reconciliation releases what the wallet reserved and drops the record without fabricating a failure event. What does reach the user is the failure LDK persisted at shutdown and replays at startup — once, with parameters only when it still matches a kept record. The restart tests cover both cases: a dropped splice-out surfaces without parameters and a further restart stays silent, while a dropped fee bump — whose record reconciliation keeps, since LDK still holds the negotiated splice — surfaces with the bump's parameters. In both, the application re-initiates and the splice completes. A splice confirmed while its node was offline keeps exactly one payment record under its splice-time id regardless of whether wallet sync or classification sees the confirmation first. Three more cases: a second splice submitted right after a zero-conf lock gets a record of its own rather than being folded into the record of the splice that just locked; a queued splice the node stopped on, which LDK fails as it shuts down, is reported at startup with its parameters — its record, an intent that never became a payment, outlives the pending splice's graduation, and reconciliation keeps it while LDK still holds that splice; and a funding record left half-written by a stop between the signing write's two stores is dropped at the next startup instead of lingering as a payment nothing indexes. Developed with assistance from Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 3da91d4 commit d961644

2 files changed

Lines changed: 728 additions & 14 deletions

File tree

tests/common/logging.rs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,17 +197,21 @@ impl CollectingLogWriter {
197197
self.logs.lock().unwrap().clone()
198198
}
199199

200-
/// Waits up to ten seconds for a logged message containing `text`, returning whether one
201-
/// arrived. Polling beats a fixed sleep: it returns as soon as the line lands and only pays
202-
/// the full timeout when the line never comes.
200+
/// Waits up to [`INTEROP_TIMEOUT_SECS`] for a logged message containing `text`, returning
201+
/// whether one arrived. Polling beats a fixed sleep: it returns as soon as the line lands and
202+
/// only pays the full timeout when the line never comes.
203+
///
204+
/// [`INTEROP_TIMEOUT_SECS`]: super::INTEROP_TIMEOUT_SECS
203205
pub(crate) async fn wait_for(&self, text: &str) -> bool {
204206
self.wait_for_count(text, 1).await
205207
}
206208

207-
/// Waits up to ten seconds for `occurrences` logged messages containing `text`, returning
208-
/// whether they arrived.
209+
/// Waits up to [`INTEROP_TIMEOUT_SECS`] for `occurrences` logged messages containing `text`,
210+
/// returning whether they arrived.
211+
///
212+
/// [`INTEROP_TIMEOUT_SECS`]: super::INTEROP_TIMEOUT_SECS
209213
pub(crate) async fn wait_for_count(&self, text: &str, occurrences: usize) -> bool {
210-
for _ in 0..100 {
214+
for _ in 0..(super::INTEROP_TIMEOUT_SECS * 10) {
211215
if self.count(text) >= occurrences {
212216
return true;
213217
}
@@ -222,3 +226,30 @@ impl LogWriter for CollectingLogWriter {
222226
self.logs.lock().unwrap().push(record.args.to_string());
223227
}
224228
}
229+
230+
/// Forwards every record to an inner [`CollectingLogWriter`] and signals `seen` when a record
231+
/// contains `marker`. The signal fires from inside the logging call, so a test can react within
232+
/// the emitting code path's timing — where the collector's polling `wait_for` (100ms granularity)
233+
/// is too coarse.
234+
pub(crate) struct MarkerLogWriter {
235+
inner: Arc<CollectingLogWriter>,
236+
marker: &'static str,
237+
seen: Arc<tokio::sync::Notify>,
238+
}
239+
240+
impl MarkerLogWriter {
241+
pub(crate) fn new(
242+
inner: Arc<CollectingLogWriter>, marker: &'static str, seen: Arc<tokio::sync::Notify>,
243+
) -> Self {
244+
Self { inner, marker, seen }
245+
}
246+
}
247+
248+
impl LogWriter for MarkerLogWriter {
249+
fn log(&self, record: LogRecord) {
250+
if record.args.to_string().contains(self.marker) {
251+
self.seen.notify_one();
252+
}
253+
LogWriter::log(&*self.inner, record);
254+
}
255+
}

0 commit comments

Comments
 (0)