detached b04eecd

fix(download): apply review follow-ups

commit
b04eecd8454e3139749aa0fc2a53738c4df4f93a
author
brice · · 2026-04-19 21:08 UTC
parent
401758e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +43 -9
M internal/infrastructure/cef/download_handler.go +9 -4
@@ -197,17 +197,22 @@ func (h *downloadHandler) currentState(id uint32, item purecef.DownloadItem) cef
197197 h.mu.Lock()
198198 defer h.mu.Unlock()
199199
200- state, ok := h.active[id]
201- if !ok {
200+ state, exists := h.active[id]
201+ if !exists {
202202 state = cefDownloadState{}
203203 }
204+ updated := false
204205 if state.destination == "" {
205206 state.destination = item.GetFullPath()
207+ updated = true
206208 }
207209 if state.filename == "" {
208210 state.filename = downloadFilenameFromItem(item, state.destination)
211+ updated = true
212+ }
213+ if exists && updated {
214+ h.active[id] = state
209215 }
210- h.active[id] = state
211216 return state
212217 }
213218
@@ -269,10 +274,10 @@ func (h *downloadHandler) markFinished(id uint32) bool {
269274 h.mu.Lock()
270275 defer h.mu.Unlock()
271276
277+ delete(h.active, id)
272278 if _, seen := h.finished[id]; seen {
273279 return false
274280 }
275- delete(h.active, id)
276281 if len(h.finished) >= maxFinishedEntries {
277282 clear(h.finished)
278283 }
@@ -197,17 +197,22 @@ func (h *downloadHandler) currentState(id uint32, item purecef.DownloadItem) cef
197 h.mu.Lock()197 h.mu.Lock()
198 defer h.mu.Unlock()198 defer h.mu.Unlock()
199 199
200- state, ok := h.active[id]200+ state, exists := h.active[id]
201- if !ok {201+ if !exists {
202 state = cefDownloadState{}202 state = cefDownloadState{}
203 }203 }
204+ updated := false
204 if state.destination == "" {205 if state.destination == "" {
205 state.destination = item.GetFullPath()206 state.destination = item.GetFullPath()
207+ updated = true
206 }208 }
207 if state.filename == "" {209 if state.filename == "" {
208 state.filename = downloadFilenameFromItem(item, state.destination)210 state.filename = downloadFilenameFromItem(item, state.destination)
211+ updated = true
212+ }
213+ if exists && updated {
214+ h.active[id] = state
209 }215 }
210- h.active[id] = state
211 return state216 return state
212 }217 }
213 218
@@ -269,10 +274,10 @@ func (h *downloadHandler) markFinished(id uint32) bool {
269 h.mu.Lock()274 h.mu.Lock()
270 defer h.mu.Unlock()275 defer h.mu.Unlock()
271 276
277+ delete(h.active, id)
272 if _, seen := h.finished[id]; seen {278 if _, seen := h.finished[id]; seen {
273 return false279 return false
274 }280 }
275- delete(h.active, id)
276 if len(h.finished) >= maxFinishedEntries {281 if len(h.finished) >= maxFinishedEntries {
277 clear(h.finished)282 clear(h.finished)
278 }283 }
M internal/infrastructure/cef/download_handler_test.go +9 -1
@@ -2,6 +2,7 @@ package cef
22
33 import (
44 "context"
5+ "fmt"
56 "testing"
67
78 purecef "github.com/bnema/purego-cef/cef"
@@ -127,6 +128,12 @@ func TestMarkFinished_SuppressesDuplicatesAfterCleanup(t *testing.T) {
127128 handler.mu.Unlock()
128129
129130 require.True(t, handler.markFinished(42), "first terminal event should succeed")
131+
132+ // Simulate a concurrent update re-populating active before a duplicate terminal update.
133+ handler.mu.Lock()
134+ handler.active[42] = cefDownloadState{filename: "test.bin", destination: "/tmp/test.bin", lastProgressPercent: -1}
135+ handler.mu.Unlock()
136+
130137 require.False(t, handler.markFinished(42), "second terminal event should be suppressed")
131138
132139 // Active map should not contain the entry.
@@ -194,6 +201,7 @@ func TestCEFDownloadInterruptedErrorIncludesCodeAndReason(t *testing.T) {
194201
195202 require.Len(t, events.events, 2)
196203 require.Error(t, events.events[1].Error)
197- require.ErrorContains(t, events.events[1].Error, "code=33")
204+ expectedCode := fmt.Sprintf("code=%d", purecef.DownloadInterruptReasonServerBadContent)
205+ require.ErrorContains(t, events.events[1].Error, expectedCode)
198206 require.ErrorContains(t, events.events[1].Error, "reason=server_bad_content")
199207 }
@@ -2,6 +2,7 @@ package cef
2 2
3 import (3 import (
4 "context"4 "context"
5+ "fmt"
5 "testing"6 "testing"
6 7
7 purecef "github.com/bnema/purego-cef/cef"8 purecef "github.com/bnema/purego-cef/cef"
@@ -127,6 +128,12 @@ func TestMarkFinished_SuppressesDuplicatesAfterCleanup(t *testing.T) {
127 handler.mu.Unlock()128 handler.mu.Unlock()
128 129
129 require.True(t, handler.markFinished(42), "first terminal event should succeed")130 require.True(t, handler.markFinished(42), "first terminal event should succeed")
131+
132+ // Simulate a concurrent update re-populating active before a duplicate terminal update.
133+ handler.mu.Lock()
134+ handler.active[42] = cefDownloadState{filename: "test.bin", destination: "/tmp/test.bin", lastProgressPercent: -1}
135+ handler.mu.Unlock()
136+
130 require.False(t, handler.markFinished(42), "second terminal event should be suppressed")137 require.False(t, handler.markFinished(42), "second terminal event should be suppressed")
131 138
132 // Active map should not contain the entry.139 // Active map should not contain the entry.
@@ -194,6 +201,7 @@ func TestCEFDownloadInterruptedErrorIncludesCodeAndReason(t *testing.T) {
194 201
195 require.Len(t, events.events, 2)202 require.Len(t, events.events, 2)
196 require.Error(t, events.events[1].Error)203 require.Error(t, events.events[1].Error)
197- require.ErrorContains(t, events.events[1].Error, "code=33")204+ expectedCode := fmt.Sprintf("code=%d", purecef.DownloadInterruptReasonServerBadContent)
205+ require.ErrorContains(t, events.events[1].Error, expectedCode)
198 require.ErrorContains(t, events.events[1].Error, "reason=server_bad_content")206 require.ErrorContains(t, events.events[1].Error, "reason=server_bad_content")
199 }207 }
M internal/infrastructure/cef/webview_handlers_test.go +7 -2
@@ -6,7 +6,6 @@ import (
66 "testing"
77 "time"
88
9- "github.com/bnema/dumber/internal/application/usecase"
109 purecef "github.com/bnema/purego-cef/cef"
1110 cefmocks "github.com/bnema/purego-cef/cef/mocks"
1211 "github.com/stretchr/testify/assert"
@@ -24,6 +23,12 @@ type clipboardOrchestratorRecorder struct {
2423 selectionCalls int
2524 }
2625
26+type stubDownloadPreparer struct{}
27+
28+func (stubDownloadPreparer) Execute(context.Context, port.DownloadPrepareInput) *port.DownloadPrepareOutput {
29+ return &port.DownloadPrepareOutput{}
30+}
31+
2732 type controlledSelectionTimer struct {
2833 stopped bool
2934 }
@@ -377,7 +382,7 @@ func TestOptionalHandlersAreAlwaysEnabled(t *testing.T) {
377382
378383 func TestGetDownloadHandler_EnabledWhenEngineConfigured(t *testing.T) {
379384 eng := &Engine{}
380- require.NoError(t, eng.ConfigureDownloads(context.Background(), "/tmp/downloads", nil, usecase.NewPrepareDownloadUseCase(nil)))
385+ require.NoError(t, eng.ConfigureDownloads(context.Background(), "/tmp/downloads", nil, stubDownloadPreparer{}))
381386
382387 h := &handlerSet{
383388 wv: &WebView{
@@ -6,7 +6,6 @@ import (
6 "testing"6 "testing"
7 "time"7 "time"
8 8
9- "github.com/bnema/dumber/internal/application/usecase"
10 purecef "github.com/bnema/purego-cef/cef"9 purecef "github.com/bnema/purego-cef/cef"
11 cefmocks "github.com/bnema/purego-cef/cef/mocks"10 cefmocks "github.com/bnema/purego-cef/cef/mocks"
12 "github.com/stretchr/testify/assert"11 "github.com/stretchr/testify/assert"
@@ -24,6 +23,12 @@ type clipboardOrchestratorRecorder struct {
24 selectionCalls int23 selectionCalls int
25 }24 }
26 25
26+type stubDownloadPreparer struct{}
27+
28+func (stubDownloadPreparer) Execute(context.Context, port.DownloadPrepareInput) *port.DownloadPrepareOutput {
29+ return &port.DownloadPrepareOutput{}
30+}
31+
27 type controlledSelectionTimer struct {32 type controlledSelectionTimer struct {
28 stopped bool33 stopped bool
29 }34 }
@@ -377,7 +382,7 @@ func TestOptionalHandlersAreAlwaysEnabled(t *testing.T) {
377 382
378 func TestGetDownloadHandler_EnabledWhenEngineConfigured(t *testing.T) {383 func TestGetDownloadHandler_EnabledWhenEngineConfigured(t *testing.T) {
379 eng := &Engine{}384 eng := &Engine{}
380- require.NoError(t, eng.ConfigureDownloads(context.Background(), "/tmp/downloads", nil, usecase.NewPrepareDownloadUseCase(nil)))385+ require.NoError(t, eng.ConfigureDownloads(context.Background(), "/tmp/downloads", nil, stubDownloadPreparer{}))
381 386
382 h := &handlerSet{387 h := &handlerSet{
383 wv: &WebView{388 wv: &WebView{
M internal/infrastructure/downloadruntime/runtime.go +7 -1
@@ -2,6 +2,7 @@ package downloadruntime
22
33 import (
44 "context"
5+ "errors"
56 "fmt"
67 "os"
78 "sync"
@@ -51,6 +52,10 @@ func (r *Runtime) ResolveDestination(
5152 preparer := r.preparer
5253 r.mu.RUnlock()
5354
55+ if preparer == nil {
56+ return nil, fmt.Errorf("download preparer is required")
57+ }
58+
5459 if err := os.MkdirAll(downloadPath, dirPerm); err != nil {
5560 return nil, fmt.Errorf("failed to create download directory %q: %w", downloadPath, err)
5661 }
@@ -149,7 +154,8 @@ func (r *Runtime) EmitFailed(ctx context.Context, filename, destination string,
149154 Err(err).
150155 Str("filename", filename).
151156 Str("destination", destination)
152- if codedErr, ok := err.(codedDownloadError); ok {
157+ var codedErr codedDownloadError
158+ if errors.As(err, &codedErr) {
153159 log = log.
154160 Int("error_code", codedErr.DownloadErrorCode()).
155161 Str("error_reason", codedErr.DownloadErrorReason())
@@ -2,6 +2,7 @@ package downloadruntime
2 2
3 import (3 import (
4 "context"4 "context"
5+ "errors"
5 "fmt"6 "fmt"
6 "os"7 "os"
7 "sync"8 "sync"
@@ -51,6 +52,10 @@ func (r *Runtime) ResolveDestination(
51 preparer := r.preparer52 preparer := r.preparer
52 r.mu.RUnlock()53 r.mu.RUnlock()
53 54
55+ if preparer == nil {
56+ return nil, fmt.Errorf("download preparer is required")
57+ }
58+
54 if err := os.MkdirAll(downloadPath, dirPerm); err != nil {59 if err := os.MkdirAll(downloadPath, dirPerm); err != nil {
55 return nil, fmt.Errorf("failed to create download directory %q: %w", downloadPath, err)60 return nil, fmt.Errorf("failed to create download directory %q: %w", downloadPath, err)
56 }61 }
@@ -149,7 +154,8 @@ func (r *Runtime) EmitFailed(ctx context.Context, filename, destination string,
149 Err(err).154 Err(err).
150 Str("filename", filename).155 Str("filename", filename).
151 Str("destination", destination)156 Str("destination", destination)
152- if codedErr, ok := err.(codedDownloadError); ok {157+ var codedErr codedDownloadError
158+ if errors.As(err, &codedErr) {
153 log = log.159 log = log.
154 Int("error_code", codedErr.DownloadErrorCode()).160 Int("error_code", codedErr.DownloadErrorCode()).
155 Str("error_reason", codedErr.DownloadErrorReason())161 Str("error_reason", codedErr.DownloadErrorReason())
M internal/infrastructure/downloadruntime/runtime_test.go +11 -1
@@ -30,7 +30,7 @@ func (c *captureEvents) OnDownloadEvent(_ context.Context, event port.DownloadEv
3030 func TestRuntimeResolveDestinationAndEvents(t *testing.T) {
3131 ctx := context.Background()
3232 events := &captureEvents{}
33- runtime := New("/tmp/downloads", events, usecase.NewPrepareDownloadUseCase(nil))
33+ runtime := New(t.TempDir(), events, usecase.NewPrepareDownloadUseCase(nil))
3434
3535 output, err := runtime.ResolveDestination(ctx, "artifact", stubResponse{mimeType: "application/pdf"})
3636
@@ -54,3 +54,13 @@ func TestNewRuntime_NilPreparer_NoPanic(t *testing.T) {
5454 _ = New("/tmp/downloads", nil, nil)
5555 })
5656 }
57+
58+func TestRuntimeResolveDestination_NilPreparerReturnsError(t *testing.T) {
59+ runtime := New(t.TempDir(), nil, nil)
60+
61+ output, err := runtime.ResolveDestination(context.Background(), "artifact", stubResponse{})
62+
63+ require.Nil(t, output)
64+ require.Error(t, err)
65+ require.ErrorContains(t, err, "download preparer is required")
66+}
@@ -30,7 +30,7 @@ func (c *captureEvents) OnDownloadEvent(_ context.Context, event port.DownloadEv
30 func TestRuntimeResolveDestinationAndEvents(t *testing.T) {30 func TestRuntimeResolveDestinationAndEvents(t *testing.T) {
31 ctx := context.Background()31 ctx := context.Background()
32 events := &captureEvents{}32 events := &captureEvents{}
33- runtime := New("/tmp/downloads", events, usecase.NewPrepareDownloadUseCase(nil))33+ runtime := New(t.TempDir(), events, usecase.NewPrepareDownloadUseCase(nil))
34 34
35 output, err := runtime.ResolveDestination(ctx, "artifact", stubResponse{mimeType: "application/pdf"})35 output, err := runtime.ResolveDestination(ctx, "artifact", stubResponse{mimeType: "application/pdf"})
36 36
@@ -54,3 +54,13 @@ func TestNewRuntime_NilPreparer_NoPanic(t *testing.T) {
54 _ = New("/tmp/downloads", nil, nil)54 _ = New("/tmp/downloads", nil, nil)
55 })55 })
56 }56 }
57+
58+func TestRuntimeResolveDestination_NilPreparerReturnsError(t *testing.T) {
59+ runtime := New(t.TempDir(), nil, nil)
60+
61+ output, err := runtime.ResolveDestination(context.Background(), "artifact", stubResponse{})
62+
63+ require.Nil(t, output)
64+ require.Error(t, err)
65+ require.ErrorContains(t, err, "download preparer is required")
66+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close