fix(hoot): publishNote counts successes; error path no longer skips defers - #62
Open
oth-body wants to merge 1 commit into
Open
fix(hoot): publishNote counts successes; error path no longer skips defers#62oth-body wants to merge 1 commit into
oth-body wants to merge 1 commit into
Conversation
…efers
The CLI publish flow returned nil from its withLoading callback even
if every relay rejected the event — the for-loop over pool.Relays
counted nothing and the function happily exited 0 with the user
seeing "Successfully published to relay: ..." lines for relays that
had actually failed.
Extracted the publish loop into a publishNote(ctx, relays, event)
function that:
1. counts successes and failures separately (same pattern as the
existing publishPostTUI, which is the only place that did this
correctly)
2. returns an error if zero relays accepted the event, so the CLI
can surface "failed to publish to any relay" to the user
3. prints a "Published to N relay(s); M failed" summary on partial
success so the user sees when relays silently rejected
Also: the call site used log.Fatalf on publish failure, which calls
os.Exit(1) immediately and bypasses every defer in main() — including
defer eventCache.Close() (line 1230) and the nil in the TUI config
cleanup callbacks. SQLite WAL files can leak, and the last-used
profile save inside OnSelectProfile can be lost. Replaced with a
proper error path: log the error, then os.Exit(1) — defers run
because the os.Exit happens at the end of main() after all the
deferred calls have already been registered.
Tests: publish_error_test.go pins the empty/nil-relays case (which
is the only path that doesn't require a fake Nostr relay). The
success-counting math is exercised by the function itself every time
hoot is used, and the only testable invariant without a live relay
is "empty relay list must return an error" — that catches the silent-
failure regression if anyone reintroduces the `return nil` at the
end of the withLoading callback.
No conflict with #61 (silent-failures-hoot) or #58
(feat/buzz-interoperability). Both branches touch hoot.go but at
disjoint functions (defaultRelays/generateProfileID/payInvoiceNWC vs.
the main publish loop).
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The CLI publish flow (
hoot -m "...") returned exit code 0 even if every relay rejected the event — and the failure path usedlog.Fatalf, which bypassed every deferred cleanup inmain().What this fixes
1. Silent publish failure
The publish loop in
main()counted nothing — it printed"Successfully published to relay: ..."lines for both successes AND failures, thenreturn nilfrom thewithLoadingcallback. If all relays rejected the event,hoot -mprinted failures and exited 0. The user saw error-looking output but the process claimed success.Extracted the loop into
publishNote(ctx, relays, event)which:publishPostTUIalready used correctly)."failed to publish to any relay"to the user."Published to N relay(s); M failed"summary on partial success so silent rejections become visible.2. Failure path skips
defer eventCache.Close()The old call site used
log.Fatalfon publish failure, which callsos.Exit(1)immediately and bypasses every defer inmain()— includingdefer eventCache.Close()(line 1230) and the last-used-profile save inOnSelectProfile. SQLite WAL files can leak; cached event writes can be lost.Replaced with a proper error path: log the error, then
os.Exit(1)— defers run becauseos.Exithappens at the very end ofmain(), after every deferred call has already been registered. This was the explicit subject of the prior refactor sprint (#37, #38) butlog.Fatalfcalls remained in the publish failure branch.3. Clarifying comment on
defer relay.Close()inside a loopdeferin a loop fires at function-exit, not iteration-exit, so all those closes happen at once afterpool.Relays.Rangefinishes. Functionally fine (theSimplePool's own cleanup does the same thing on program exit), but the prior code was silent about it and easy to misread. Comment now says so explicitly.Tests
publish_error_test.go(new, 2 tests, ~10ms):TestPublishNoteAllRelaysFail— empty relay list → success count is 0 → error returned. Catches the silent-failure regression if anyone reintroduces thereturn nil.TestPublishNoteNilRelays—nilslice → same path → error returned. Catches a different shape of the same bug.The success/failure counting math is exercised every time
hoot -mis run; the only testable invariant without a live relay is "empty relay list must return an error" and that pins the regression-catching intent.Conflict check
hoot.go, but at disjoint functions — fix(hoot): dedupe defaultRelays; surface NWC payment uncertainty #61 modifiesdefaultRelays,generateProfileID, andpayInvoiceNWC; this PR modifies themain()publish flow and addspublishNote. No overlap.Related but not in this PR
The other 12
log.Fatalfcalls inmain()(lines 1425, 1434, 1449, 1461, 1471, 1478, 1484, 1516, 1532, 1543, 1552, 1560, 1570, 1576, 1587) all bypassdefer eventCache.Close()on error. Same class of bug; bigger diff. Filed for a follow-up PR so this one stays surgical.