From a30f595d5de0ffa7ede89bd2527d7168a650309b Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Tue, 30 Jun 2026 11:47:40 +0200 Subject: [PATCH 1/2] feat(probe): include route fee in reports --- test/helpers/probe.ts | 57 +++++++++++++++++++++++++++------ test/specs/mainnet/probe.e2e.ts | 24 ++++++++------ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/test/helpers/probe.ts b/test/helpers/probe.ts index a90358e..c922752 100644 --- a/test/helpers/probe.ts +++ b/test/helpers/probe.ts @@ -29,6 +29,7 @@ export type ProbeResult = { invoiceFetched: boolean; success: boolean; durationMs: number; + routeFeeMsat?: number; bolt11?: string; nodeId?: string; rawProviderResult?: string; @@ -49,6 +50,12 @@ type LnurlInvoiceResponse = { reason?: string; }; +export type ProbeCommandResult = { + success: boolean; + durationMs?: number; + routeFeeMsat?: number; +}; + const DEFAULT_PROBE_TIMEOUT_SECONDS = 90; const DEFAULT_PROBE_FETCH_RETRIES = 2; const DEFAULT_PROBE_FETCH_RETRY_DELAY_MS = 1_000; @@ -234,24 +241,48 @@ export function runProbeNodeCommand(target: ProbeTarget, amountMsat: number): st return runDevToolsCommand(method, payload, timeoutSeconds); } -export function parseProbeCommandSuccess(raw: string): boolean { +export function parseProbeCommandResult(raw: string): ProbeCommandResult | null { const result = extractContentCallResult(raw); - if (!result) return false; + if (!result) return null; let parsed: unknown; try { parsed = JSON.parse(result); } catch { - return false; + return null; } - if (typeof parsed !== 'object' || parsed === null) return false; + if (typeof parsed !== 'object' || parsed === null) return null; - if ('success' in parsed) return parsed.success === true; + const payload = parsed as Record; if ('type' in parsed && typeof parsed.type === 'string') { - return parsed.type === 'Success' || parsed.type.endsWith('.ProbeSuccess'); + return { + success: parsed.type === 'Success' || parsed.type.endsWith('.ProbeSuccess'), + durationMs: parseOptionalNumber(payload.durationMs), + routeFeeMsat: parseOptionalNumber(payload.routeFeeMsat), + }; + } + + return { + success: payload.success === true, + durationMs: parseOptionalNumber(payload.durationMs), + routeFeeMsat: parseOptionalNumber(payload.routeFeeMsat), + }; +} + +export function parseProbeCommandSuccess(raw: string): boolean { + return parseProbeCommandResult(raw)?.success ?? false; +} + +function parseOptionalNumber(value: unknown): number | undefined { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + if (typeof value === 'string') { + const parsedValue = Number.parseInt(value, 10); + return Number.isFinite(parsedValue) ? parsedValue : undefined; } - return false; + return undefined; } export function summarizeProbeCommandFailure(raw: string): string { @@ -557,8 +588,8 @@ export function renderProbeReport( `Scores reset: ${scoresResetForReport()}`, `Readiness at probe start: ${readiness ? summarizeProbeReadiness(readiness) : 'not captured'}`, '', - '| Target | Type | Amount sats | Required | Fetch | Probe | Retries | Duration ms | Failure |', - '| --- | --- | ---: | --- | --- | --- | ---: | ---: | --- |', + '| Target | Type | Amount sats | Required | Fetch | Probe | Retries | Duration ms | Route fee msat | Failure |', + '| --- | --- | ---: | --- | --- | --- | ---: | ---: | ---: | --- |', ]; for (const result of results) { @@ -572,6 +603,7 @@ export function renderProbeReport( result.success ? '✅' : '❌', result.retries.toString(), result.durationMs.toString(), + formatRouteFeeCell(result), result.success ? '' : formatFailureCell(result.error ?? ''), ].join(' | ')} |` ); @@ -684,6 +716,9 @@ async function fetchJsonOnce(url: string): Promise { if (!response.ok) { throw new Error(`HTTP ${response.status} for ${url}${formatResponseBody(text)}`); } + if (text.trim().length === 0) { + throw new Error(`HTTP ${response.status} for ${url} returned an empty response body`); + } return JSON.parse(text) as T; } @@ -759,6 +794,10 @@ function formatFetchCell(result: ProbeResult): string { return result.invoiceFetched ? 'ok' : 'failed'; } +function formatRouteFeeCell(result: ProbeResult): string { + return result.routeFeeMsat === undefined ? '' : result.routeFeeMsat.toString(); +} + function runDevToolsCommand( method: string, payload: Record, diff --git a/test/specs/mainnet/probe.e2e.ts b/test/specs/mainnet/probe.e2e.ts index 8a873ee..5c3aecc 100644 --- a/test/specs/mainnet/probe.e2e.ts +++ b/test/specs/mainnet/probe.e2e.ts @@ -4,7 +4,7 @@ import { buildProbeQueue, fetchBolt11ForProbe, parseNonNegativeIntEnv, - parseProbeCommandSuccess, + parseProbeCommandResult, probeModeForTargetType, resolveProbeAmountProfile, resetPathfindingScores, @@ -88,15 +88,16 @@ async function runInvoiceProbe(target: ProbeTarget, amountMsat: number): Promise ); const rawProviderResult = runProbeInvoiceCommand(target, amountMsat, bolt11); lastRawProviderResult = rawProviderResult; - const success = parseProbeCommandSuccess(rawProviderResult); + const providerResult = parseProbeCommandResult(rawProviderResult); - if (success) { + if (providerResult?.success) { return { ...baseResult, retries: retry, invoiceFetched: true, success: true, - durationMs: Date.now() - startedAt, + durationMs: providerResult.durationMs ?? Date.now() - startedAt, + routeFeeMsat: providerResult.routeFeeMsat, bolt11, rawProviderResult, }; @@ -113,12 +114,14 @@ async function runInvoiceProbe(target: ProbeTarget, amountMsat: number): Promise } } + const providerResult = parseProbeCommandResult(lastRawProviderResult); return { ...baseResult, retries: maxRetries, invoiceFetched: true, success: false, - durationMs: Date.now() - startedAt, + durationMs: providerResult?.durationMs ?? Date.now() - startedAt, + routeFeeMsat: providerResult?.routeFeeMsat, bolt11, rawProviderResult: lastRawProviderResult, error: lastError, @@ -159,14 +162,15 @@ async function runNodeProbe(target: ProbeTarget, amountMsat: number): Promise Date: Tue, 30 Jun 2026 12:53:10 +0200 Subject: [PATCH 2/2] fix(probe): split dev result parsing helpers --- test/helpers/probe.ts | 51 ++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/test/helpers/probe.ts b/test/helpers/probe.ts index c922752..b0bbf8c 100644 --- a/test/helpers/probe.ts +++ b/test/helpers/probe.ts @@ -241,7 +241,7 @@ export function runProbeNodeCommand(target: ProbeTarget, amountMsat: number): st return runDevToolsCommand(method, payload, timeoutSeconds); } -export function parseProbeCommandResult(raw: string): ProbeCommandResult | null { +function parseDevResultPayload(raw: string): Record | null { const result = extractContentCallResult(raw); if (!result) return null; @@ -253,24 +253,32 @@ export function parseProbeCommandResult(raw: string): ProbeCommandResult | null } if (typeof parsed !== 'object' || parsed === null) return null; - const payload = parsed as Record; - if ('type' in parsed && typeof parsed.type === 'string') { - return { - success: parsed.type === 'Success' || parsed.type.endsWith('.ProbeSuccess'), - durationMs: parseOptionalNumber(payload.durationMs), - routeFeeMsat: parseOptionalNumber(payload.routeFeeMsat), - }; - } + return parsed as Record; +} + +function parseDevResultSuccess(payload: Record): boolean { + if ('success' in payload) return payload.success === true; + + const type = payload.type; + return ( + typeof type === 'string' && (type === 'Success' || type.endsWith('.ProbeSuccess')) + ); +} + +export function parseProbeCommandResult(raw: string): ProbeCommandResult | null { + const payload = parseDevResultPayload(raw); + if (!payload) return null; return { - success: payload.success === true, + success: parseDevResultSuccess(payload), durationMs: parseOptionalNumber(payload.durationMs), routeFeeMsat: parseOptionalNumber(payload.routeFeeMsat), }; } export function parseProbeCommandSuccess(raw: string): boolean { - return parseProbeCommandResult(raw)?.success ?? false; + const payload = parseDevResultPayload(raw); + return payload ? parseDevResultSuccess(payload) : false; } function parseOptionalNumber(value: unknown): number | undefined { @@ -330,10 +338,11 @@ export async function resetPathfindingScores({ console.info(`→ [${logPrefix}] Resetting pathfinding scores (timeout ${timeoutSeconds}s)...`); const fallbackFloorS = getDeviceEpochSeconds(); const raw = runDevToolsCommand(method, {}, timeoutSeconds); - if (!parseProbeCommandSuccess(raw)) { + const payload = parseDevResultPayload(raw); + if (!payload || !parseDevResultSuccess(payload)) { throw new Error(`Pathfinding scores reset failed: ${summarizeProbeCommandFailure(raw)}`); } - const deviceResetAtS = parseResetTimestamp(raw); + const deviceResetAtS = parseResetTimestamp(payload); if (deviceResetAtS === null) { console.warn( `→ [${logPrefix}] Reset result has no timestamp (old app build?); using pre-reset device time as scores sync floor` @@ -344,20 +353,8 @@ export async function resetPathfindingScores({ return resetFloorS; } -function parseResetTimestamp(raw: string): number | null { - const result = extractContentCallResult(raw); - if (!result) return null; - - let parsed: unknown; - try { - parsed = JSON.parse(result); - } catch { - return null; - } - if (typeof parsed !== 'object' || parsed === null) return null; - if (!('timestamp' in parsed)) return null; - - const timestamp = parsed.timestamp; +function parseResetTimestamp(payload: Record): number | null { + const timestamp = payload.timestamp; return typeof timestamp === 'number' && Number.isFinite(timestamp) && timestamp > 0 ? timestamp : null;