Skip to content

Overnight/test harness 2026 04 20#7

Merged
claudenstein merged 68 commits into
mainfrom
overnight/test-harness-2026-04-20
Apr 28, 2026
Merged

Overnight/test harness 2026 04 20#7
claudenstein merged 68 commits into
mainfrom
overnight/test-harness-2026-04-20

Conversation

@claudenstein

Copy link
Copy Markdown
Owner

No description provided.

Frank Dosk and others added 30 commits April 27, 2026 14:35
Same pattern as the AllTorrentDocs and Stats pagination covers:
1001 content docs under one infohash forces ContentDocsForInfoHash's
second loop iteration with from=1000, exercising the
`from += batch` increment at indexer.go:485.

Skipped under -short (indexing 1001 docs takes ~10s).

Coverage:
- indexer.go ContentDocsForInfoHash 90.9% → 95.5%
- internal/indexer total           96.9% → 97.1%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
newRightClickCapture and CreateRenderer move 0% → 100%.
TappedSecondary covers nil-menuBuilder, nil-menu, empty-items,
and nil-canvas early-returns at 88.9% (the
ShowPopUpMenuAtPosition success path requires a real canvas
attachment which fyne/test doesn't drive).

Coverage:
- context_menu.go newRightClickCapture 0% → 100%
- context_menu.go CreateRenderer       0% → 100%
- context_menu.go TappedSecondary      0% → 88.9%
- internal/gui total                   9.9% → 10.8%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both methods are testable without a daemon — SelectTab uses only
the tabs container, and Cleanup calls a.cancel(). Construct App
directly with the relevant fields and exercise:

- SelectTab dispatches by all 10 accepted names (5 tabs × 2
  spellings) and silently ignores unknown names.
- SelectTab nil-tabs short-circuit at app.go:393-395.
- Cleanup invokes the stored cancel func, observable via the
  paired ctx.

Coverage:
- app.go SelectTab 0% → 100%
- app.go Cleanup   0% → 100%
- internal/gui     10.8% → 11.5%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two tab-construction helpers that don't need a daemon: labelRow
returns an HBox of (bold-name, value-label) and makeLabelGroup
allocates N "-" placeholder labels. Construct under test.NewApp
and verify shape + count.

Coverage:
- status.go labelRow       0% → 100%
- status.go makeLabelGroup 0% → 100%
- internal/gui total       11.5% → 12.0%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sortFilesLocked dispatches on fd.sortBy across path/size/progress/
priority/index plus the prioRank table inside the priority case.
Construct a filesDialog with synthetic FileSnapshots — no daemon
needed — and assert the post-sort ordering for each sort key
(plus an "unknown" key that falls through to the index default).

Coverage:
- files_dialog.go sortFilesLocked 0% → 100%
- internal/gui total              12.0% → 13.7%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-case cover:
- negative sortCol short-circuits at downloads.go:434.
- sortCol >= 0 invokes snapLess + sortSnapsSlice (already 100%)
  and produces the documented ascending order.

Coverage:
- downloads.go sortSnapsLocked 0% → 100%
- internal/gui total           13.7% → 14.1%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
toggleSort cycles asc → desc → cleared on the active column,
switches to ascending on a different column, and short-circuits
on out-of-range column indices. Construct a downloadsTab with a
real (but stub-callback) widget.Table so Refresh works inside
test.NewApp, and walk every transition.

Coverage:
- downloads.go toggleSort 0% → 100%
- internal/gui total      14.1% → 15.0%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four one-line wrappers — companionTab.win(), downloadsTab.win(),
searchTab.win(), settingsTab.win() — each returning
windowForObject(t.content). With no canvas attachment they fall
back to the first known window. Construct each tab with a label
content under test.NewApp + an anchor window and assert non-nil
return.

Coverage:
- companion.go win  0% → 100%
- downloads.go win  0% → 100%
- search.go win     0% → 100%
- settings.go win   0% → 100%
- internal/gui      15.0% → 15.3%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
unfollowAt's `if ct.d.CompSub == nil || idx >= len(ct.follows)`
short-circuit at companion.go:255 covers two pre-decode guards.
Construct a companionTab with a bare daemon.Daemon{} (nil CompSub)
and exercise both nil-CompSub + out-of-range halves. The
DecodeString err arm requires a real CompSub and is left
uncovered for now.

Coverage:
- companion.go unfollowAt 0% → 22.2%
- internal/gui total      15.3% → 15.4%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more daemon-free helpers — refreshPublisher's
`if ct.d.CompPub == nil { return }` arm and doFollow's first
arm `if ct.d.CompSub == nil → ShowError + return`. Both build
on a bare daemon.Daemon{} and (for doFollow) a fyne/test app +
anchor window so dialog.ShowError has a parent. The second/third
doFollow arms (len-check + bad-hex) need a real
SubscriberWorker; left for a future testlab integration test.

Coverage:
- companion.go refreshPublisher 0% → 28.6%
- companion.go doFollow         0% → 23.1%
- internal/gui total            15.4% → 15.8%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
selectedInfoHash returns dl.snaps[dl.selected].InfoHash with two
out-of-range guards. Five sub-cases cover -1 sentinel, past-end
index, two valid indices, and an empty snaps slice. Daemon-free.

Coverage:
- downloads.go selectedInfoHash 0% → 100%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
showAddMagnetError builds an information dialog, wires a re-open
callback to OnClosed, and shows it. Daemon-free — only needs a
fyne/test app + anchor window. The OnClosed callback path
(which calls showAddMagnetDialogPrefilled) requires closing the
dialog, which test.NewApp doesn't drive interactively, so it
remains uncovered.

Coverage:
- downloads.go showAddMagnetError 0% → 75%
- internal/gui total              15.8% → 16.1%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests for the daemon-free arms of several gui methods:
selectedInfoHash early-returns in pause/resume/remove/toggle/show
files; showSignatureDialog unsigned arm; runSearch empty-query
guard; showAddMagnetDialog/Prefilled dialog construction;
showAddFileDialog construction; removeSelected confirm-dialog
construction (named + unnamed labels). gui pkg coverage 16.1% →
19.7%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests for daemon-free arms of three more gui paths:

- buildContextMenu: nil-return on no selection, plus four snap
  variants (paused/unpaused, indexing/not, queued, signed
  trusted/untrusted) so every label/section branch fires;
  daemon calls live inside menu-item callbacks that don't run
- applyQueueSettings/applyRateLimits: invalid-input ShowError
  arms before any daemon call
- createTorrentDialog: full dialog construction (rootEntry,
  pieceSelect, trackers/webseeds, sign/seed checks, output
  path picker); daemon only used inside OnConfirm so passing
  nil is safe

gui pkg coverage 19.7% → 26.3%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests for daemon-free arms across four more gui paths:

- installShortcuts: full keyboard-shortcut wiring (Ctrl+N/F/Q,
  Delete) with closures captured but not invoked
- setupSystemTray: non-desktop type-assertion early-return arm
  (Fyne's test app is not desktop.App)
- makeLocalHitCard / makeSwarmHitCard / makeDHTHitCard: card
  construction across each subtitle-extension arm (empty/non-
  empty Name, content vs torrent DocType, signed-by, BloomHit,
  Sources/Size present)
- buildResults: all six branches — local/swarm/dht resp arm,
  local/swarm/dht err arm, plus the no-layers fallback parts
  append. Card callbacks reference daemon methods but never
  fire during construction.

gui pkg coverage 26.3% → 32.2%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests for daemon-free arms in gui pollLoop / refresh paths:

- companionTab.refresh: nil CompPub + nil CompSub arms execute
  the gather-skip paths and the fyne.Do label-reset block;
  refresh 0% → 43.2%
- companionTab.pollLoop / downloadsTab.pollLoop /
  filesDialog.pollLoop: canceled-context fast-return arm fires
  before any daemon dereference

gui pkg coverage 32.2% → 34.7%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests for daemon-free arms across more gui paths:

- titleLoop / notificationLoop: canceled-ctx fast-return arm
  (no daemon dereference before select)
- newDownloadsTab: full constructor (table, headers, toolbar
  buttons, empty-state); canceled context makes the
  background pollLoop return on first iteration
- newSearchTab: pure constructor — daemon used only in
  callbacks, no goroutine
- newCompanionTab: constructor + nil-safe pollLoop refresh
  (CompPub / CompSub both nil) — empty daemon.Daemon{}
  suffices

gui pkg coverage 34.7% → 41.9%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a test covering setAllPriorities at files_dialog.go:219
when fd.files is empty: the synchronous body snapshots an empty
index list, the goroutine's for-loop body skips, and
fd.d.Eng.SetFilePriority is never called. setAllPriorities
0% → 61.5%; gui total 41.9% → 42.5%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds daemon-backed tests that lift several previously-0% gui
methods. Introduces a newTestDaemon helper that spins up a
minimal daemon (no DHT, no upload, no identity / reputation /
bloom / trust paths) — enough wiring for engine getters and
indexer Stats/Search to run without segfaulting.

- newStatusTab + statusTab.refresh (0% / 0% → 76.7% / 69.1%):
  full card construction and one refresh under a real engine
- newSettingsTab + load/apply paths: loadCurrent, loadRateLimits,
  loadQueueSettings + the success arms of applyRateLimits,
  applyQueueSettings, save
- confirmHit / flagHit: bad-hex, wrong-length-decoded, and happy
  paths against a non-nil bloom + tracker
- runSearch goroutine: happy-path local query and the all-layers-
  off fallback "No search layers enabled" status

gui pkg coverage 42.5% → 56.7%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds daemon-backed tests for several previously-0% gui paths:

- showFilesDialog: err arm against an unknown infohash (engine
  returns "torrent not found")
- filesDialog.build: full dialog + pollLoop construction with
  hand-injected files; dlg.Hide() exits the goroutine cleanly
- App.showAbout: dialog renders with empty pubKey (no identity),
  non-zero LocalPort, and apiAddr "disabled" arms
- runCreateTorrent: missing-Root err arm AND happy-path success
  (creates a real .torrent and re-adds it via AddTorrentMetaInfo
  for the "Seeding started" message)

gui pkg coverage 56.7% → 62.5%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an addTestTorrent helper that creates a real torrent file
via Engine.CreateTorrentFile + AddTorrentMetaInfo, polling
TorrentSnapshots until the new infohash appears. Two daemon-
backed tests use it:

- pauseSelected / resumeSelected / toggleIndexSelected /
  showFilesForSelected: each runs against a real engine + real
  torrent so the daemon-touching arms execute (previously only
  the nil-selection early-returns)
- showSignatureDialog: trusted + untrusted variants — both fire
  the TrustStore.Label() lookup and render the trust labels;
  previously only the unsigned-early-return arm was covered

gui pkg coverage 62.5% → 65.7%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds daemon-backed tests that let the long-running goroutines
fire at least one tick before cancellation:

- downloadsTab.pollLoop: 2.2s wait covers the tick.C body —
  TorrentSnapshots fetch, sort under lock, table.Refresh, and
  empty-state toggle
- titleLoop + notificationLoop: combined into a single 3.2s
  wait so total wall time stays under ~3.5s; both fetch
  snapshots and update window title / fire notifications

gui pkg coverage 65.7% → 67.1%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Setting BloomPath + ReputationPath under TempDir lets the engine
construct a non-nil KnownGoodBloom and ReputationTracker, which
in turn lets confirmHit / flagHit reach their bloom.Add and
RecordConfirmed/RecordFlagged arms. Two new tests pre-seed the
SourceTracker so the `len(pks) > 0` happy path also fires.

confirmHit 21.4% → 92.9%, flagHit 18.8% → 87.5%; gui pkg total
67.1% → 69.0%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an IdentityPath under TempDir so the engine constructs a real
Identity (helps showAbout pubKey arm) and a new test that
pre-records reputation entries (one short pubkey, one >16-char to
exercise the truncation arm) before status.refresh runs.

status.refresh 72.2% → 79.4%; gui pkg 69.0% → 69.6%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a newDHTTestDaemon helper that builds a daemon with DHT
enabled (DisableDHTPublish=true) and CompanionDir set so the
engine wires up CompSub + CompPub. Three previously-uncovered
companion-tab arms now fire end-to-end:

- doFollow happy path: 64-char hex pubkey decodes, CompSub.Follow
  runs (23.1% → 69.2%)
- unfollowAt happy path: valid follow row, CompSub.Unfollow runs
  (22.2% → 88.9%)
- refreshPublisher: CompPub.RefreshNow goroutine spawns and
  returns cleanly with no indexed content (28.6% → 71.4%)

gui pkg coverage 69.6% → 70.7%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new daemon-backed tests using newDHTTestDaemon exercise the
non-nil-Lookup / non-nil-Bootstrap / non-nil-Publisher arms in
status.refresh, and the non-nil-CompPub / non-nil-CompSub arms
in companion.refresh — including the follow-loop body after
CompSub.Follow seeds a row.

companion.refresh 43.2% → 94.6%, status.refresh 79.4% → 84.5%;
gui pkg total 70.7% → 72.6%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more gui tests:

- setAllPriorities failed arm: with files populated and an
  unknown infohash, the inner SetFilePriority loop populates the
  failed slice and fyne.Do fires ShowError
- newCompanionTab with CompSub wired: pre-seed a follow so the
  follow-list UpdateCell callback runs against a real row during
  the initial refresh

gui pkg coverage 72.6% → 73.0%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mounting dl.content in a test window and resizing to 1600x800
forces Fyne to lay out the downloads table and call the
UpdateCell callback for every visible row, exercising every
column's cell-formatting branch (Name with empty + non-empty,
Status, Progress, Size, Peers, rates, Indexed yes/no, Signed
none/trusted/untrusted).

newDownloadsTab 34.9% → 77.9%; gui pkg total 73.0% → 75.8%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mount ct.content in a 1200x800 test window plus a second
seeded follow with a long pubkey label so the >16-char
truncation arm fires. Companion follow-list UpdateCell
callback now executes its per-row body for both seeded
entries.

newCompanionTab 60.0% → ~80%; gui pkg total 75.8% → 76.7%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A daemon-backed test combines DHT enabled (so Publisher and
Lookup are wired) with a real torrent in flight (so the snaps
loop body and per-status switch fire). status.refresh 84.5% →
89.7%; gui pkg total 76.7% → 77.1%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frank Dosk and others added 28 commits April 27, 2026 22:11
The test canvas's embedded ShortcutHandler lets us call
TypedShortcut for each Ctrl+N/F/Q binding so the handler
closures execute (Add Magnet dialog, focus Search, quit).
The OnTypedKey handler is fetched via Canvas.OnTypedKey() and
invoked with KeyDelete to fire the row-delete arm.

installShortcuts 47.4% → 84.2%; gui pkg total 82.5% → 83.1%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A small helper recursively walks a CanvasObject tree to find a
button by label. The companion tab's "Refresh Now" and "Follow"
buttons are deep inside cards/containers, so direct handles
aren't available — the walker locates them and we invoke each
OnTapped closure.

newCompanionTab 87.5% → 92.5%; gui pkg total 83.1% → 83.4%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
findButtonByLabel locates each toolbar button (Add Magnet,
Add .torrent, Create Torrent, Pause, Resume, Remove, Toggle
Index, Files...) and we invoke its OnTapped closure. With
selected=-1 the per-row actions early-return but the closure
prefix and dialog-construction paths execute.

newDownloadsTab 86.0% → 95.3%; gui pkg total 83.4% → 84.0%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After fd.build() the dialog content is added as an overlay
on the parent window's canvas. test.LaidOutObjects walks
the laid-out child tree including PopUp/Card wrappers, so we
locate the Select All / Deselect All buttons by Text and
invoke their OnTapped closures (which delegate to
fd.setAllPriorities).

filesDialog.build 86.0% → 89.5%; gui pkg total 84.0% → 84.2%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After createTorrentDialog opens, find the Choose File/Folder/
Save As buttons via test.LaidOutObjects on the canvas overlays
and invoke each OnTapped — the closure body that constructs
and shows a file/folder dialog runs (the file-pick callback
itself only fires on user pick).

createTorrentDialog 44.9% → 53.8%; gui pkg total 84.2% → 84.7%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locate rootEntry by its placeholder text via test.LaidOutObjects
on the canvas overlays, then SetText with three variants:
slash-only (autofillName early-return on base ".") , a real
path (initial fill), and a second real path (re-fill since
nameEntry.Text == lastAutofill).

createTorrentDialog 53.8% → 62.8%; gui pkg total 84.7% → 85.3%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the dialog is shown the URI Entry and Add button live in
the canvas overlay stack. We locate them via test.LaidOutObjects
and tap Add with three URIs to drive each submit-callback arm:
empty (showAddMagnetError), validateMagnetURI rejection
(showAddMagnetError), and a valid magnet (engine AddMagnetURI
goroutine fires).

showAddMagnetDialogPrefilled 40.0% → 83.3%; gui pkg total
85.3% → 86.3%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walking the canvas overlays after removeSelected shows the
confirm dialog, locate the default "Yes" button (fyne uses
Yes/No labels for ShowConfirm) and tap OnTapped. The
confirmation goroutine then invokes Eng.RemoveTorrent and
flips selected back to -1.

removeSelected 63.6% → 95.5%; gui pkg total 86.3% → 86.8%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walk the canvas overlays after showAddMagnetError, locate the
information dialog's "OK" button (fyne's default dismiss), and
invoke OnTapped. d.Hide() then fires the SetOnClosed callback
which re-opens showAddMagnetDialogPrefilled.

showAddMagnetError 75.0% → 100%; gui pkg total 86.8% → 86.9%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three opens of the dialog, each tapping its "Create" button via
overlay-walker:

- empty rootEntry → ShowError "root path required"
- root set, output empty → ShowError "output path required"
- valid root + output → runCreateTorrent goroutine fires (the
  hashing fails because the path doesn't exist, but the
  closure body up through that point executes)

createTorrentDialog 62.8% → 78.2%; gui pkg total 86.9% → 87.8%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- flagHit fallback: pre-record a tracker entry but flag an
  infohash with no recorded sources — the function falls
  through to iterate tracker.Snapshot, populating pks from
  every known indexer. flagHit 87.5% → 93.8%
- status + companion pollLoop tick.C arms: combined into one
  ~4.2s wait so both 4s tickers fire once before cancellation.
  Both 85.7% → 100%

gui pkg total 87.8% → 88.0%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pollLoop tick tests:

- TestDownloadsPollLoopTickEmpty: no torrents added → snaps
  stays empty → emptyState.Show() arm fires
- TestDownloadsPollLoopTick (updated): adds a torrent first
  → snaps non-empty → emptyState.Hide() arm fires

downloads.pollLoop 93.3% → 100%; gui pkg total 88.0% → 88.1%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits each newXxxTab function into a buildXxxTab (struct +
widget construction, no goroutine) plus a thin newXxxTab
(buildXxxTab + go pollLoop). Tests use the build* variant to
drive refresh/status logic without the pollLoop goroutine
racing test-thread widget reads on shared Fyne caches.

- newStatusTab: now a 3-line wrapper over buildStatusTab; tests
  in status_with_daemon, refresh_with_dht, status_dht_with_torrent,
  status_full_dht, status_with_reputation switch over
- newCompanionTab: same split; companion_buttons,
  new_companion_tab[_with_dht], refresh_with_dht switch over
- newDownloadsTab: same split (no test usage change yet but the
  helper is available)
- build_context_menu_actions: skips goroutine-spawning menu items
  (Pause/Resume/Toggle Index/Queue moves) since their leaked
  Eng.* goroutines race other tests' Fyne caches under -race
- companion_buttons: stops tapping Refresh Now (its goroutine
  bleeds); just verifies the button is wired up
- removes status_companion_pollloop_tick_test — its purpose was
  to exercise the 4s tick.C arms, but the pollLoop goroutines it
  spawns leak across the whole test sweep and race other tests

Behavior unchanged in production (newXxxTab still spawns
pollLoop). gui pkg coverage 88.1% → 86.2%; the trade-off is
worth it for cleaner -race behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The valid-magnet arm tapped the dialog's Add button with a
real magnet URI, which spawned a goroutine that ran
Eng.AddMagnetURI + Eng.SetTorrentIndexing. That goroutine
bleeds across the test boundary and races other gui tests'
Fyne caches under -race. Empty-URI and bad-scheme arms (the
validation paths) still cover the closure body without
spawning engine goroutines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Several tests added in this branch tap goroutine-spawning UI
paths (runCreateTorrent, RemoveTorrent confirm, runSearch
multi-layer goroutines, refreshPublisher's goroutine). Their
spawned goroutines outlive the test and race other tests' Fyne
work under -race. The goroutine bodies are mostly covered by
direct unit tests of confirmHit/flagHit/buildResults; the
bleeding wrapper tests are net-negative for -race compliance.

Removed: run_create_torrent_test.go, create_dialog_confirm_test.go,
remove_selected_confirm_test.go, run_search_dht_test.go,
TestRunSearchHappyPath / TestRunSearchAllLayersOff (in
search_with_daemon_test.go), TestCompanionRefreshPublisherWithDaemon
(in companion_with_compsub_test.go).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more bleeders went away:

- TestSetAllPrioritiesFailedArm: setAllPriorities with non-empty
  files spawns a goroutine that errs and pushes ShowError via
  fyne.Do; the queued Fyne work bleeds into next tests
- TestFilesDialogBuildWithPrefilledFiles: fd.build spawns the
  2 s-tick pollLoop on its own context; goroutine outlives the
  test (its OnClosed cancel only fires on dialog interaction)
- TestDownloadsSelectedActionsHappyPath: showFilesForSelected
  goes through the same fd.build path
- TestPersistFnsNoSessReturnEarly (engine): writes eng.sess = nil
  after AddMagnet which has spawned upgradeMagnetSession reading
  eng.sess — true engine race, not just goroutine bleed. Removed
  rather than serialise; the nil-sess defensive arms are
  reachable via the existing engine.Close happy-path tests.

Full CI-equivalent race sweep
(go test -race -count=1 $pkgs, where pkgs excludes
internal/testlab) is now CLEAN across cmd/swartznet,
cmd/swartznet-gui, internal/{companion, config, daemon,
dhtindex, engine, gui, httpapi, httpapi/web, identity,
indexer, indexer/extractors, reputation, signing,
swarmsearch, trust}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous version called eng.AddMagnet which spawned an
upgradeMagnetSession goroutine that read e.sess concurrently
with the test's e.sess = nil write. The new version skips
AddMagnet entirely: each persist*/RestoreSession/upgradeMagnetSession
function checks `if e.sess == nil { return }` BEFORE touching its
*Handle argument, so we can pass nil and still hit the
nil-sess defensive arm.

persistAdd / persistState / RestoreSession back to 100%
(upgradeMagnetSession 87% same as before because that fn's
non-nil-sess path already had an existing test).

Full -race sweep across all CI-tested packages remains clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TestBuildResultsArms and TestSetAllPrioritiesEmptyFiles racing
each other ~3/8 of the time on a clean -race sweep. Both touch
Fyne widgets which share unsynchronised global font caches
upstream. With t.Parallel removed they run sequentially and
the flake disappears (10/10 clean across consecutive runs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calling pollLoop directly with a canceled context exercises
the initial-refresh + ctx.Done early-return arm without
spawning the goroutine that bleeds across test boundaries.
status/companion pollLoop both 0% → ~85%; gui pkg 74.0% → 74.5%.
-race sweep stable across 5 consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
downloads.pollLoop has no pre-loop refresh (unlike status/
companion pollLoops), so a canceled-ctx newDownloadsTab call
spawns the goroutine which exits on the first select without
any fyne.Do bleed. Wrapper goes 0% → 100%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both wrappers spawn pollLoop goroutines that fire an initial
refresh; with a canceled ctx the goroutine exits after the
refresh. A 500 ms drain at end of test lets the fyne.Do
callbacks complete before the test returns so they don't bleed
into other tests' Fyne work.

newStatusTab / newCompanionTab 0% → 100%; gui pkg 74.5% → 74.9%.
-race stable across 5 consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calls runCreateTorrent with a non-existent Root path: the
goroutine errs quickly inside CreateTorrentFile, then fyne.Do
hits the ShowError + return arm. A 500 ms drain at end of test
lets the goroutine fully complete so its work doesn't bleed.

runCreateTorrent 0% → 60%; gui pkg 74.9% → 75.6%. -race stable
across 5 consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fd.build spawns a pollLoop goroutine on its own internal context,
cancelled via SetOnClosed. We hand-populate files, call build,
immediately Hide() the dialog (fires OnClosed → cancel), then
sleep 100 ms to drain the goroutine before the test returns.

build 0% → 86%; gui pkg 75.6% → 79.4%. -race stable across 5
consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CompPub.RefreshNow goroutine completes quickly on a fresh
daemon (no indexed content → no work → returns nil); the
fyne.Do(ShowError) arm doesn't fire so the goroutine doesn't
bleed Fyne work. A 200 ms drain keeps it bounded.

refreshPublisher 28.6% → 71.4%; gui pkg 79.4% → 79.6%. -race
stable across 5 consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restored TestSetAllPrioritiesWithFiles (with 500 ms drain after
calling setAllPriorities on non-empty files + unknown infohash).
The goroutine's SetFilePriority loop fails per-file, populates
failed slice, fyne.Do(ShowError) fires — all bounded within the
drain window so the goroutine doesn't bleed.

setAllPriorities 61.5% → 100%; gui pkg 79.6% → 80.0%. -race
stable across 5 consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With all layer checkboxes off, runSearch's orchestrator
goroutine spawns no per-layer goroutines, waits on an empty
WaitGroup, then fyne.Do's buildResults to write the "No search
layers enabled" status message. A 500 ms drain at end of test
lets the goroutine fully complete; the test does NOT read
statusLbl.Text from the test goroutine since that would race
with buildResults' SetText.

runSearch 6.4% → 61.7%; gui pkg 80.0% → 82.0%. -race stable
across 5 consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restored TestRemoveSelectedConfirmAction. After tapping the
confirm dialog's Yes button the goroutine fires Eng.RemoveTorrent
+ flips selected via fyne.Do; a 500 ms drain bounds it.

removeSelected 63.6% → 95.5%; gui pkg 82.0% → 82.6%. -race
stable across 5 consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restored TestCreateTorrentDialogConfirmArms with three input
variants (empty root, empty output, valid both). The valid arm
spawns runCreateTorrent's goroutine; a 500 ms drain bounds it.

createTorrentDialog 62.8% → 78.2%; gui pkg 82.6% → 83.5%.
-race stable across 5 consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claudenstein
claudenstein merged commit 239165c into main Apr 28, 2026
1 check failed
claudenstein pushed a commit that referenced this pull request Jul 19, 2026
Closes the round-4 adversarial backlog; each fix has a regression test that
fails when the fix is neutralized. All touched packages race-clean.

- #10 companion: put the new BEP-46 pointer BEFORE dropping the old seed, so a
  failed put keeps the still-advertised infohash fetchable.
- #7 dhtindex: TOCTOU between off-lock aggregate Refresh and a concurrent
  Retract — added a gen counter + post-distribute re-check so a retracted tree
  isn't left published for a full refresh interval.
- #9 indexer/httpapi/daemon: a malformed query string is now a typed
  ErrBadQuery → HTTP 400 (client fault), not 500; classified at the daemon
  seam via a LocalBadRequest flag (httpapi imports no indexer types).
- #5 companion: publisher GeneratedAt is now strictly monotonic across content
  changes, so a backward wall-clock step no longer trips the follower's replay
  guard and drops legitimate new content.
- #11 companion: an Unfollow racing an in-flight Sync no longer resurrects the
  dedup maps — per-publisher Forget epoch, re-checked under lock in commitDedup.
- #4 engine: persistAdd runs after Add releases e.mu; a racing RemoveTorrent
  could be resurrected — updateGuarded(abort) + Handle.isRemoved() make the
  create happens-before-correct (RemoveTorrent closes h.removed before its
  lock-guarded remove); persistSeedAdd DataPath refinement → updateExisting.
- #8 swarmsearch: initiator/responder txid collision deferred with a fix sketch
  (needs a wire direction bit + the unwired StartSync path) — see DECISIONS T3-8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant