detached 2d810ef

fix(release): address final review blockers

commit
2d810ef49146bfe99fc935286b6c76d28eef3bca
author
brice · · 2026-06-27 06:48 UTC
committer
brice · · 2026-06-27 06:56 UTC
parent
c0d5b53
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
15 files · +200 -61
M .goreleaser.yaml +2 -1
@@ -4,8 +4,9 @@ version: 2
44
55 before:
66 hooks:
7- # Build systemviews assets before building binaries
7+ # Build and verify systemviews assets before building binaries
88 - make build-systemviews
9+ - make verify-systemviews-assets
910
1011 builds:
1112 # Pure Go build (no CGO required)
@@ -4,8 +4,9 @@ version: 2
4 4
5 before:5 before:
6 hooks:6 hooks:
7- # Build systemviews assets before building binaries7+ # Build and verify systemviews assets before building binaries
8 - make build-systemviews8 - make build-systemviews
9+ - make verify-systemviews-assets
9 10
10 builds:11 builds:
11 # Pure Go build (no CGO required)12 # Pure Go build (no CGO required)
M Makefile +5 -1
@@ -1,6 +1,6 @@
11 # Makefile for dumber (Clean Architecture - puregotk)
22
3-.PHONY: build build-systemviews generate-systemviews build-quick install-local test lint staticcheck clean install-tools install-golangci-lint install-staticcheck dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego verify-generated check
3+.PHONY: build build-systemviews generate-systemviews build-quick install-local test lint staticcheck clean install-tools install-golangci-lint install-staticcheck dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego verify-generated verify-systemviews-assets check
44
55 # Load local overrides from .env.local if present (Makefile syntax)
66 ifneq (,$(wildcard .env.local))
@@ -136,6 +136,10 @@ verify-generated: ## Verify generated systemviews artifacts are committed
136136 exit 1; \
137137 }
138138
139+verify-systemviews-assets: ## Verify release embeds a compressed systemviews WASM asset
140+ @echo "Verifying embedded systemviews WASM asset..."
141+ DUMBER_REQUIRE_SYSTEMVIEWS_WASM=1 GOFLAGS=$(GOFLAGS) go test -count=1 -run TestWebUIAssetsIncludesCompressedSystemviewsWASM ./assets
142+
139143 # Linting
140144 lint: ## Run golangci-lint
141145 @echo "Running golangci-lint..."
@@ -1,6 +1,6 @@
1 # Makefile for dumber (Clean Architecture - puregotk)1 # Makefile for dumber (Clean Architecture - puregotk)
2 2
3-.PHONY: build build-systemviews generate-systemviews build-quick install-local test lint staticcheck clean install-tools install-golangci-lint install-staticcheck dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego verify-generated check3+.PHONY: build build-systemviews generate-systemviews build-quick install-local test lint staticcheck clean install-tools install-golangci-lint install-staticcheck dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego verify-generated verify-systemviews-assets check
4 4
5 # Load local overrides from .env.local if present (Makefile syntax)5 # Load local overrides from .env.local if present (Makefile syntax)
6 ifneq (,$(wildcard .env.local))6 ifneq (,$(wildcard .env.local))
@@ -136,6 +136,10 @@ verify-generated: ## Verify generated systemviews artifacts are committed
136 exit 1; \136 exit 1; \
137 }137 }
138 138
139+verify-systemviews-assets: ## Verify release embeds a compressed systemviews WASM asset
140+ @echo "Verifying embedded systemviews WASM asset..."
141+ DUMBER_REQUIRE_SYSTEMVIEWS_WASM=1 GOFLAGS=$(GOFLAGS) go test -count=1 -run TestWebUIAssetsIncludesCompressedSystemviewsWASM ./assets
142+
139 # Linting143 # Linting
140 lint: ## Run golangci-lint144 lint: ## Run golangci-lint
141 @echo "Running golangci-lint..."145 @echo "Running golangci-lint..."
A assets/webui_embed_test.go +34 -0
@@ -0,0 +1,34 @@
1+package assets
2+
3+import (
4+ "bytes"
5+ "io"
6+ "os"
7+ "testing"
8+
9+ "github.com/andybalholm/brotli"
10+)
11+
12+func TestWebUIAssetsIncludesCompressedSystemviewsWASM(t *testing.T) {
13+ compressed, err := WebUIAssets.ReadFile("systemviews/systemviews.wasm.br")
14+ if err != nil {
15+ if os.Getenv("DUMBER_REQUIRE_SYSTEMVIEWS_WASM") == "" {
16+ t.Skipf("generated systemviews WASM asset not present; run make build-systemviews: %v", err)
17+ }
18+ t.Fatalf("read embedded systemviews WASM asset: %v", err)
19+ }
20+ if len(compressed) == 0 {
21+ t.Fatal("embedded systemviews WASM asset is empty")
22+ }
23+
24+ reader := brotli.NewReader(bytes.NewReader(compressed))
25+ header := make([]byte, 8)
26+ if _, err := io.ReadFull(reader, header); err != nil {
27+ t.Fatalf("decompress embedded systemviews WASM header: %v", err)
28+ }
29+
30+ wantHeader := []byte{0x00, 'a', 's', 'm', 0x01, 0x00, 0x00, 0x00}
31+ if !bytes.Equal(header, wantHeader) {
32+ t.Fatalf("embedded systemviews asset is not a WASM module: got header % x", header)
33+ }
34+}
@@ -0,0 +1,34 @@
1+package assets
2+
3+import (
4+ "bytes"
5+ "io"
6+ "os"
7+ "testing"
8+
9+ "github.com/andybalholm/brotli"
10+)
11+
12+func TestWebUIAssetsIncludesCompressedSystemviewsWASM(t *testing.T) {
13+ compressed, err := WebUIAssets.ReadFile("systemviews/systemviews.wasm.br")
14+ if err != nil {
15+ if os.Getenv("DUMBER_REQUIRE_SYSTEMVIEWS_WASM") == "" {
16+ t.Skipf("generated systemviews WASM asset not present; run make build-systemviews: %v", err)
17+ }
18+ t.Fatalf("read embedded systemviews WASM asset: %v", err)
19+ }
20+ if len(compressed) == 0 {
21+ t.Fatal("embedded systemviews WASM asset is empty")
22+ }
23+
24+ reader := brotli.NewReader(bytes.NewReader(compressed))
25+ header := make([]byte, 8)
26+ if _, err := io.ReadFull(reader, header); err != nil {
27+ t.Fatalf("decompress embedded systemviews WASM header: %v", err)
28+ }
29+
30+ wantHeader := []byte{0x00, 'a', 's', 'm', 0x01, 0x00, 0x00, 0x00}
31+ if !bytes.Equal(header, wantHeader) {
32+ t.Fatalf("embedded systemviews asset is not a WASM module: got header % x", header)
33+ }
34+}
M docs/config/index.md +16 -14
@@ -149,27 +149,29 @@ auto_open_on_new_pane = false
149149 ### Color Palettes
150150
151151 **Light palette:**
152+
152153 ```toml
153154 [appearance.light_palette]
154-background = "#f8f8f8"
155-surface = "#f2f2f2"
156-surface_variant = "#ececec"
157-text = "#1a1a1a"
158-muted = "#6e6e6e"
159-accent = "#404040"
160-border = "#d2d2d2"
155+background = "#fafafa"
156+surface = "#f4f4f5"
157+surface_variant = "#e4e4e7"
158+text = "#18181b"
159+muted = "#71717a"
160+accent = "#22c55e"
161+border = "#d4d4d8"
161162 ```
162163
163164 **Dark palette:**
165+
164166 ```toml
165167 [appearance.dark_palette]
166-background = "#0e0e0e"
167-surface = "#1a1a1a"
168-surface_variant = "#141414"
169-text = "#e4e4e4"
170-muted = "#848484"
171-accent = "#a8a8a8"
172-border = "#363636"
168+background = "#0a0a0b"
169+surface = "#18181b"
170+surface_variant = "#27272a"
171+text = "#fafafa"
172+muted = "#a1a1aa"
173+accent = "#4ade80"
174+border = "#3f3f46"
173175 ```
174176
175177 ### External Theme: Noctalia
@@ -149,27 +149,29 @@ auto_open_on_new_pane = false
149 ### Color Palettes149 ### Color Palettes
150 150
151 **Light palette:**151 **Light palette:**
152+
152 ```toml153 ```toml
153 [appearance.light_palette]154 [appearance.light_palette]
154-background = "#f8f8f8"155+background = "#fafafa"
155-surface = "#f2f2f2"156+surface = "#f4f4f5"
156-surface_variant = "#ececec"157+surface_variant = "#e4e4e7"
157-text = "#1a1a1a"158+text = "#18181b"
158-muted = "#6e6e6e"159+muted = "#71717a"
159-accent = "#404040"160+accent = "#22c55e"
160-border = "#d2d2d2"161+border = "#d4d4d8"
161 ```162 ```
162 163
163 **Dark palette:**164 **Dark palette:**
165+
164 ```toml166 ```toml
165 [appearance.dark_palette]167 [appearance.dark_palette]
166-background = "#0e0e0e"168+background = "#0a0a0b"
167-surface = "#1a1a1a"169+surface = "#18181b"
168-surface_variant = "#141414"170+surface_variant = "#27272a"
169-text = "#e4e4e4"171+text = "#fafafa"
170-muted = "#848484"172+muted = "#a1a1aa"
171-accent = "#a8a8a8"173+accent = "#4ade80"
172-border = "#363636"174+border = "#3f3f46"
173 ```175 ```
174 176
175 ### External Theme: Noctalia177 ### External Theme: Noctalia
M docs/reference/configuration.md +14 -14
@@ -38,20 +38,20 @@
3838 | `appearance.external_theme.provider` | string | `noctalia` | `noctalia` |
3939 | `appearance.external_theme.format` | string | `colors-json` | `colors-json`, `dumber-json` |
4040 | `appearance.external_theme.path` | string | `$XDG_CONFIG_HOME/noctalia/colors.json` | path to Noctalia `colors.json` or a `dumber-json` theme file; when empty and `format = "dumber-json"`, dumber uses the built-in dumber-json template path |
41-| `appearance.light_palette.background` | string | `#f8f8f8` | |
42-| `appearance.light_palette.surface` | string | `#f2f2f2` | |
43-| `appearance.light_palette.surface_variant` | string | `#ececec` | |
44-| `appearance.light_palette.text` | string | `#1a1a1a` | |
45-| `appearance.light_palette.muted` | string | `#6e6e6e` | |
46-| `appearance.light_palette.accent` | string | `#404040` | |
47-| `appearance.light_palette.border` | string | `#d2d2d2` | |
48-| `appearance.dark_palette.background` | string | `#0e0e0e` | |
49-| `appearance.dark_palette.surface` | string | `#1a1a1a` | |
50-| `appearance.dark_palette.surface_variant` | string | `#141414` | |
51-| `appearance.dark_palette.text` | string | `#e4e4e4` | |
52-| `appearance.dark_palette.muted` | string | `#848484` | |
53-| `appearance.dark_palette.accent` | string | `#a8a8a8` | |
54-| `appearance.dark_palette.border` | string | `#363636` | |
41+| `appearance.light_palette.background` | string | `#fafafa` | |
42+| `appearance.light_palette.surface` | string | `#f4f4f5` | |
43+| `appearance.light_palette.surface_variant` | string | `#e4e4e7` | |
44+| `appearance.light_palette.text` | string | `#18181b` | |
45+| `appearance.light_palette.muted` | string | `#71717a` | |
46+| `appearance.light_palette.accent` | string | `#22c55e` | |
47+| `appearance.light_palette.border` | string | `#d4d4d8` | |
48+| `appearance.dark_palette.background` | string | `#0a0a0b` | |
49+| `appearance.dark_palette.surface` | string | `#18181b` | |
50+| `appearance.dark_palette.surface_variant` | string | `#27272a` | |
51+| `appearance.dark_palette.text` | string | `#fafafa` | |
52+| `appearance.dark_palette.muted` | string | `#a1a1aa` | |
53+| `appearance.dark_palette.accent` | string | `#4ade80` | |
54+| `appearance.dark_palette.border` | string | `#3f3f46` | |
5555 | `debug.enable_devtools` | bool | `true` | |
5656 | `engine.cef.log_file` | string | `` | CEF runtime log path |
5757 | `engine.cef.log_severity` | int32 | `0` | `0`, `1`, `2`, `3`, `4`, `99` |
@@ -38,20 +38,20 @@
38 | `appearance.external_theme.provider` | string | `noctalia` | `noctalia` |38 | `appearance.external_theme.provider` | string | `noctalia` | `noctalia` |
39 | `appearance.external_theme.format` | string | `colors-json` | `colors-json`, `dumber-json` |39 | `appearance.external_theme.format` | string | `colors-json` | `colors-json`, `dumber-json` |
40 | `appearance.external_theme.path` | string | `$XDG_CONFIG_HOME/noctalia/colors.json` | path to Noctalia `colors.json` or a `dumber-json` theme file; when empty and `format = "dumber-json"`, dumber uses the built-in dumber-json template path |40 | `appearance.external_theme.path` | string | `$XDG_CONFIG_HOME/noctalia/colors.json` | path to Noctalia `colors.json` or a `dumber-json` theme file; when empty and `format = "dumber-json"`, dumber uses the built-in dumber-json template path |
41-| `appearance.light_palette.background` | string | `#f8f8f8` | |41+| `appearance.light_palette.background` | string | `#fafafa` | |
42-| `appearance.light_palette.surface` | string | `#f2f2f2` | |42+| `appearance.light_palette.surface` | string | `#f4f4f5` | |
43-| `appearance.light_palette.surface_variant` | string | `#ececec` | |43+| `appearance.light_palette.surface_variant` | string | `#e4e4e7` | |
44-| `appearance.light_palette.text` | string | `#1a1a1a` | |44+| `appearance.light_palette.text` | string | `#18181b` | |
45-| `appearance.light_palette.muted` | string | `#6e6e6e` | |45+| `appearance.light_palette.muted` | string | `#71717a` | |
46-| `appearance.light_palette.accent` | string | `#404040` | |46+| `appearance.light_palette.accent` | string | `#22c55e` | |
47-| `appearance.light_palette.border` | string | `#d2d2d2` | |47+| `appearance.light_palette.border` | string | `#d4d4d8` | |
48-| `appearance.dark_palette.background` | string | `#0e0e0e` | |48+| `appearance.dark_palette.background` | string | `#0a0a0b` | |
49-| `appearance.dark_palette.surface` | string | `#1a1a1a` | |49+| `appearance.dark_palette.surface` | string | `#18181b` | |
50-| `appearance.dark_palette.surface_variant` | string | `#141414` | |50+| `appearance.dark_palette.surface_variant` | string | `#27272a` | |
51-| `appearance.dark_palette.text` | string | `#e4e4e4` | |51+| `appearance.dark_palette.text` | string | `#fafafa` | |
52-| `appearance.dark_palette.muted` | string | `#848484` | |52+| `appearance.dark_palette.muted` | string | `#a1a1aa` | |
53-| `appearance.dark_palette.accent` | string | `#a8a8a8` | |53+| `appearance.dark_palette.accent` | string | `#4ade80` | |
54-| `appearance.dark_palette.border` | string | `#363636` | |54+| `appearance.dark_palette.border` | string | `#3f3f46` | |
55 | `debug.enable_devtools` | bool | `true` | |55 | `debug.enable_devtools` | bool | `true` | |
56 | `engine.cef.log_file` | string | `` | CEF runtime log path |56 | `engine.cef.log_file` | string | `` | CEF runtime log path |
57 | `engine.cef.log_severity` | int32 | `0` | `0`, `1`, `2`, `3`, `4`, `99` |57 | `engine.cef.log_severity` | int32 | `0` | `0`, `1`, `2`, `3`, `4`, `99` |
M internal/application/port/mocks/local_path_resolver.go +2 -2
@@ -99,8 +99,8 @@ func (_c *MockLocalPathResolver_ResolveExistingPath_Call) Run(run func(ctx conte
9999 return _c
100100 }
101101
102-func (_c *MockLocalPathResolver_ResolveExistingPath_Call) Return(s string, b bool, err error) *MockLocalPathResolver_ResolveExistingPath_Call {
103- _c.Call.Return(s, b, err)
102+func (_c *MockLocalPathResolver_ResolveExistingPath_Call) Return(absPath string, ok bool, err error) *MockLocalPathResolver_ResolveExistingPath_Call {
103+ _c.Call.Return(absPath, ok, err)
104104 return _c
105105 }
106106
@@ -99,8 +99,8 @@ func (_c *MockLocalPathResolver_ResolveExistingPath_Call) Run(run func(ctx conte
99 return _c99 return _c
100 }100 }
101 101
102-func (_c *MockLocalPathResolver_ResolveExistingPath_Call) Return(s string, b bool, err error) *MockLocalPathResolver_ResolveExistingPath_Call {102+func (_c *MockLocalPathResolver_ResolveExistingPath_Call) Return(absPath string, ok bool, err error) *MockLocalPathResolver_ResolveExistingPath_Call {
103- _c.Call.Return(s, b, err)103+ _c.Call.Return(absPath, ok, err)
104 return _c104 return _c
105 }105 }
106 106
M internal/infrastructure/cef/scheme_handler.go +1 -1
@@ -616,7 +616,7 @@ func safeSystemviewsAssetPath(assetDir, relPath string) (fullPath, cleanRelPath
616616 return fullPath, cleanRelPath, true
617617 }
618618
619-func readAssetWithEncoding(assets embed.FS, fullPath, relPath string) ([]byte, error) {
619+func readAssetWithEncoding(assets fs.FS, fullPath, relPath string) ([]byte, error) {
620620 if strings.HasSuffix(relPath, ".wasm") {
621621 if compressed, err := fs.ReadFile(assets, fullPath+".br"); err == nil {
622622 data, err := io.ReadAll(io.LimitReader(brotli.NewReader(bytes.NewReader(compressed)), maxSystemviewsWASMBytes+1))
@@ -616,7 +616,7 @@ func safeSystemviewsAssetPath(assetDir, relPath string) (fullPath, cleanRelPath
616 return fullPath, cleanRelPath, true616 return fullPath, cleanRelPath, true
617 }617 }
618 618
619-func readAssetWithEncoding(assets embed.FS, fullPath, relPath string) ([]byte, error) {619+func readAssetWithEncoding(assets fs.FS, fullPath, relPath string) ([]byte, error) {
620 if strings.HasSuffix(relPath, ".wasm") {620 if strings.HasSuffix(relPath, ".wasm") {
621 if compressed, err := fs.ReadFile(assets, fullPath+".br"); err == nil {621 if compressed, err := fs.ReadFile(assets, fullPath+".br"); err == nil {
622 data, err := io.ReadAll(io.LimitReader(brotli.NewReader(bytes.NewReader(compressed)), maxSystemviewsWASMBytes+1))622 data, err := io.ReadAll(io.LimitReader(brotli.NewReader(bytes.NewReader(compressed)), maxSystemviewsWASMBytes+1))
M internal/infrastructure/cef/scheme_handler_systemviews_test.go +24 -7
@@ -1,10 +1,12 @@
11 package cef
22
33 import (
4+ "bytes"
45 "net/url"
56 "testing"
7+ "testing/fstest"
68
7- "github.com/bnema/dumber/assets"
9+ "github.com/andybalholm/brotli"
810 )
911
1012 func TestResolveAssetPath_SystemviewsRootsUseSystemviewsShell(t *testing.T) {
@@ -42,19 +44,34 @@ func TestResolveAssetPath_SystemviewsRootsUseSystemviewsShell(t *testing.T) {
4244 }
4345 }
4446
45-func TestReadAssetWithEncoding_DecompressesEmbeddedBrotliWASM(t *testing.T) {
47+func TestReadAssetWithEncoding_DecompressesBrotliWASM(t *testing.T) {
4648 t.Parallel()
4749
48- data, err := readAssetWithEncoding(assets.WebUIAssets, "systemviews/systemviews.wasm", "systemviews.wasm")
50+ wasm := []byte("\x00asmfixture")
51+ fsys := fstest.MapFS{
52+ "systemviews/systemviews.wasm.br": {Data: brotliCompressForTest(t, wasm)},
53+ }
54+
55+ data, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
4956 if err != nil {
5057 t.Fatalf("readAssetWithEncoding() error = %v", err)
5158 }
52- if len(data) < 4 {
53- t.Fatalf("decompressed wasm too short: got %d bytes", len(data))
59+ if !bytes.Equal(data, wasm) {
60+ t.Fatalf("decompressed wasm = %q, want %q", data, wasm)
61+ }
62+}
63+
64+func brotliCompressForTest(t *testing.T, data []byte) []byte {
65+ t.Helper()
66+ var buf bytes.Buffer
67+ w := brotli.NewWriter(&buf)
68+ if _, err := w.Write(data); err != nil {
69+ t.Fatalf("brotli write: %v", err)
5470 }
55- if string(data[:4]) != "\x00asm" {
56- t.Fatalf("decompressed wasm magic = %q, want \\x00asm", data[:4])
71+ if err := w.Close(); err != nil {
72+ t.Fatalf("brotli close: %v", err)
5773 }
74+ return buf.Bytes()
5875 }
5976
6077 func mustParseURL(t *testing.T, raw string) *url.URL {
@@ -1,10 +1,12 @@
1 package cef1 package cef
2 2
3 import (3 import (
4+ "bytes"
4 "net/url"5 "net/url"
5 "testing"6 "testing"
7+ "testing/fstest"
6 8
7- "github.com/bnema/dumber/assets"9+ "github.com/andybalholm/brotli"
8 )10 )
9 11
10 func TestResolveAssetPath_SystemviewsRootsUseSystemviewsShell(t *testing.T) {12 func TestResolveAssetPath_SystemviewsRootsUseSystemviewsShell(t *testing.T) {
@@ -42,19 +44,34 @@ func TestResolveAssetPath_SystemviewsRootsUseSystemviewsShell(t *testing.T) {
42 }44 }
43 }45 }
44 46
45-func TestReadAssetWithEncoding_DecompressesEmbeddedBrotliWASM(t *testing.T) {47+func TestReadAssetWithEncoding_DecompressesBrotliWASM(t *testing.T) {
46 t.Parallel()48 t.Parallel()
47 49
48- data, err := readAssetWithEncoding(assets.WebUIAssets, "systemviews/systemviews.wasm", "systemviews.wasm")50+ wasm := []byte("\x00asmfixture")
51+ fsys := fstest.MapFS{
52+ "systemviews/systemviews.wasm.br": {Data: brotliCompressForTest(t, wasm)},
53+ }
54+
55+ data, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
49 if err != nil {56 if err != nil {
50 t.Fatalf("readAssetWithEncoding() error = %v", err)57 t.Fatalf("readAssetWithEncoding() error = %v", err)
51 }58 }
52- if len(data) < 4 {59+ if !bytes.Equal(data, wasm) {
53- t.Fatalf("decompressed wasm too short: got %d bytes", len(data))60+ t.Fatalf("decompressed wasm = %q, want %q", data, wasm)
61+ }
62+}
63+
64+func brotliCompressForTest(t *testing.T, data []byte) []byte {
65+ t.Helper()
66+ var buf bytes.Buffer
67+ w := brotli.NewWriter(&buf)
68+ if _, err := w.Write(data); err != nil {
69+ t.Fatalf("brotli write: %v", err)
54 }70 }
55- if string(data[:4]) != "\x00asm" {71+ if err := w.Close(); err != nil {
56- t.Fatalf("decompressed wasm magic = %q, want \\x00asm", data[:4])72+ t.Fatalf("brotli close: %v", err)
57 }73 }
74+ return buf.Bytes()
58 }75 }
59 76
60 func mustParseURL(t *testing.T, raw string) *url.URL {77 func mustParseURL(t *testing.T, raw string) *url.URL {
M internal/infrastructure/cef/webview_handlers_test.go +8 -4
@@ -263,14 +263,14 @@ func TestHandleNativePopupAborted_PreservesPrimedNavigationForFallback(t *testin
263263 }
264264
265265 func TestOnBeforePopup_TimesOutGTKDispatchAndBlocksPopup(t *testing.T) {
266- var delayed func()
266+ delayed := make(chan func(), 1)
267267 parentWV := &WebView{
268268 ctx: context.Background(),
269269 id: 16,
270270 gtkSyncTimeout: 5 * time.Millisecond,
271271 gtkSyncIsOwner: func() bool { return false },
272272 gtkSyncDispatch: func(fn func()) {
273- delayed = fn
273+ delayed <- fn
274274 },
275275 }
276276 var createCalls atomic.Int32
@@ -286,9 +286,13 @@ func TestOnBeforePopup_TimesOutGTKDispatchAndBlocksPopup(t *testing.T) {
286286
287287 require.True(t, blocked)
288288 require.Zero(t, createCalls.Load())
289- require.NotNil(t, delayed)
290289
291- delayed()
290+ select {
291+ case fn := <-delayed:
292+ fn()
293+ case <-time.After(time.Second):
294+ t.Fatal("timed out waiting for delayed popup dispatch")
295+ }
292296 require.Zero(t, createCalls.Load())
293297 }
294298
@@ -263,14 +263,14 @@ func TestHandleNativePopupAborted_PreservesPrimedNavigationForFallback(t *testin
263 }263 }
264 264
265 func TestOnBeforePopup_TimesOutGTKDispatchAndBlocksPopup(t *testing.T) {265 func TestOnBeforePopup_TimesOutGTKDispatchAndBlocksPopup(t *testing.T) {
266- var delayed func()266+ delayed := make(chan func(), 1)
267 parentWV := &WebView{267 parentWV := &WebView{
268 ctx: context.Background(),268 ctx: context.Background(),
269 id: 16,269 id: 16,
270 gtkSyncTimeout: 5 * time.Millisecond,270 gtkSyncTimeout: 5 * time.Millisecond,
271 gtkSyncIsOwner: func() bool { return false },271 gtkSyncIsOwner: func() bool { return false },
272 gtkSyncDispatch: func(fn func()) {272 gtkSyncDispatch: func(fn func()) {
273- delayed = fn273+ delayed <- fn
274 },274 },
275 }275 }
276 var createCalls atomic.Int32276 var createCalls atomic.Int32
@@ -286,9 +286,13 @@ func TestOnBeforePopup_TimesOutGTKDispatchAndBlocksPopup(t *testing.T) {
286 286
287 require.True(t, blocked)287 require.True(t, blocked)
288 require.Zero(t, createCalls.Load())288 require.Zero(t, createCalls.Load())
289- require.NotNil(t, delayed)
290 289
291- delayed()290+ select {
291+ case fn := <-delayed:
292+ fn()
293+ case <-time.After(time.Second):
294+ t.Fatal("timed out waiting for delayed popup dispatch")
295+ }
292 require.Zero(t, createCalls.Load())296 require.Zero(t, createCalls.Load())
293 }297 }
294 298
M internal/infrastructure/updater/github.go +5 -0
@@ -279,6 +279,11 @@ func isRetryableRequestError(err error) bool {
279279 return true
280280 }
281281
282+ var dnsErr *net.DNSError
283+ if errors.As(err, &dnsErr) && dnsErr.IsTemporary {
284+ return true
285+ }
286+
282287 var opErr *net.OpError
283288 if errors.As(err, &opErr) {
284289 if isTransientSyscallError(opErr.Err) {
@@ -279,6 +279,11 @@ func isRetryableRequestError(err error) bool {
279 return true279 return true
280 }280 }
281 281
282+ var dnsErr *net.DNSError
283+ if errors.As(err, &dnsErr) && dnsErr.IsTemporary {
284+ return true
285+ }
286+
282 var opErr *net.OpError287 var opErr *net.OpError
283 if errors.As(err, &opErr) {288 if errors.As(err, &opErr) {
284 if isTransientSyscallError(opErr.Err) {289 if isTransientSyscallError(opErr.Err) {
M internal/infrastructure/updater/github_test.go +1 -0
@@ -405,6 +405,7 @@ func TestIsRetryableRequestError(t *testing.T) {
405405 {name: "context canceled", err: context.Canceled, want: false},
406406 {name: "context deadline exceeded", err: context.DeadlineExceeded, want: true},
407407 {name: "timeout net error", err: &net.DNSError{IsTimeout: true}, want: true},
408+ {name: "temporary dns error", err: &net.DNSError{IsTemporary: true}, want: true},
408409 {name: "non-timeout net error", err: &net.DNSError{Err: "lookup failed"}, want: false},
409410 {name: "transient errno through net op error", err: &net.OpError{Op: "read", Net: "tcp", Err: syscall.ECONNRESET}, want: true},
410411 {name: "non-transient errno", err: syscall.EINVAL, want: false},
@@ -405,6 +405,7 @@ func TestIsRetryableRequestError(t *testing.T) {
405 {name: "context canceled", err: context.Canceled, want: false},405 {name: "context canceled", err: context.Canceled, want: false},
406 {name: "context deadline exceeded", err: context.DeadlineExceeded, want: true},406 {name: "context deadline exceeded", err: context.DeadlineExceeded, want: true},
407 {name: "timeout net error", err: &net.DNSError{IsTimeout: true}, want: true},407 {name: "timeout net error", err: &net.DNSError{IsTimeout: true}, want: true},
408+ {name: "temporary dns error", err: &net.DNSError{IsTemporary: true}, want: true},
408 {name: "non-timeout net error", err: &net.DNSError{Err: "lookup failed"}, want: false},409 {name: "non-timeout net error", err: &net.DNSError{Err: "lookup failed"}, want: false},
409 {name: "transient errno through net op error", err: &net.OpError{Op: "read", Net: "tcp", Err: syscall.ECONNRESET}, want: true},410 {name: "transient errno through net op error", err: &net.OpError{Op: "read", Net: "tcp", Err: syscall.ECONNRESET}, want: true},
410 {name: "non-transient errno", err: syscall.EINVAL, want: false},411 {name: "non-transient errno", err: syscall.EINVAL, want: false},
M internal/infrastructure/webkit/messaging.go +37 -12
@@ -68,7 +68,22 @@ func (r *MessageRouter) SetBaseContext(ctx context.Context) {
6868 if ctx == nil {
6969 ctx = context.Background()
7070 }
71+ r.mu.Lock()
7172 r.baseCtx = ctx
73+ r.mu.Unlock()
74+}
75+
76+func (r *MessageRouter) baseContext() context.Context {
77+ if r == nil {
78+ return context.Background()
79+ }
80+ r.mu.RLock()
81+ ctx := r.baseCtx
82+ r.mu.RUnlock()
83+ if ctx == nil {
84+ return context.Background()
85+ }
86+ return ctx
7287 }
7388
7489 // RegisterHandler registers a handler for a message type.
@@ -116,7 +131,8 @@ func (r *MessageRouter) RegisterHandlerWithCallbacks(msgType, callback, errorCal
116131 // WebKit's messageHandlers is only available in main world.
117132 // The isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler.
118133 func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ string) (uint, error) {
119- log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()
134+ baseCtx := r.baseContext()
135+ log := logging.FromContext(baseCtx).With().Str("component", "message-router").Logger()
120136
121137 log.Debug().Msg("SetupMessageHandler called")
122138
@@ -166,7 +182,8 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st
166182
167183 // handleScriptMessage decodes the JSC value and routes it to the correct handler.
168184 func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager, valuePtr uintptr) {
169- log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()
185+ baseCtx := r.baseContext()
186+ log := logging.FromContext(baseCtx).With().Str("component", "message-router").Logger()
170187
171188 log.Debug().Uint64("value_ptr", uint64(valuePtr)).Msg("handleScriptMessage entry")
172189
@@ -225,13 +242,11 @@ func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager,
225242
226243 entry, ok := r.getHandler(msg.Type)
227244 if !ok || entry.handler == nil {
228- log.Warn().Str("type", msg.Type).Int("registered_handlers", len(r.handlers)).Msg("no handler registered for message type")
229- // Debug: list all registered handlers
230- r.mu.RLock()
231- for handlerType := range r.handlers {
245+ handlerTypes := r.registeredHandlerTypes()
246+ log.Warn().Str("type", msg.Type).Int("registered_handlers", len(handlerTypes)).Msg("no handler registered for message type")
247+ for _, handlerType := range handlerTypes {
232248 log.Debug().Str("registered_type", handlerType).Msg("available handler")
233249 }
234- r.mu.RUnlock()
235250 return
236251 }
237252
@@ -242,12 +257,12 @@ func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager,
242257 Str("callback", entry.callback).
243258 Msg("received script message, dispatching to handler")
244259
245- resp, err := entry.handler.Handle(r.baseCtx, WebViewID(msg.WebViewID), msg.Payload)
260+ resp, err := entry.handler.Handle(baseCtx, WebViewID(msg.WebViewID), msg.Payload)
246261 log.Debug().Str("type", msg.Type).Err(err).Msg("handler execution completed")
247262 if err != nil {
248263 log.Error().Err(err).Str("type", msg.Type).Msg("message handler returned error")
249264 if entry.errorCallback != "" {
250- if respErr := r.dispatchResponse(r.baseCtx, WebViewID(msg.WebViewID), entry.errorCallback, entry.world, err.Error()); respErr != nil {
265+ if respErr := r.dispatchResponse(baseCtx, WebViewID(msg.WebViewID), entry.errorCallback, entry.world, err.Error()); respErr != nil {
251266 log.Warn().Err(respErr).Msg("failed to dispatch error callback")
252267 }
253268 }
@@ -255,7 +270,7 @@ func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager,
255270 }
256271
257272 if entry.callback != "" {
258- if err := r.dispatchResponse(r.baseCtx, WebViewID(msg.WebViewID), entry.callback, entry.world, resp); err != nil {
273+ if err := r.dispatchResponse(baseCtx, WebViewID(msg.WebViewID), entry.callback, entry.world, resp); err != nil {
259274 log.Warn().Err(err).
260275 Str("callback", entry.callback).
261276 Uint64("webview_id", msg.WebViewID).
@@ -291,7 +306,7 @@ func (r *MessageRouter) syncWebViewID(wv *WebView) {
291306 return
292307 }
293308 script := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(wv.ID()))
294- wv.RunJavaScriptInWorld(r.baseCtx, script, "")
309+ wv.RunJavaScriptInWorld(r.baseContext(), script, "")
295310 }
296311
297312 func (r *MessageRouter) markWebViewIDSynced(id WebViewID) bool {
@@ -312,10 +327,20 @@ func (r *MessageRouter) getHandler(msgType string) (handlerEntry, bool) {
312327 return entry, ok
313328 }
314329
330+func (r *MessageRouter) registeredHandlerTypes() []string {
331+ r.mu.RLock()
332+ defer r.mu.RUnlock()
333+ types := make([]string, 0, len(r.handlers))
334+ for handlerType := range r.handlers {
335+ types = append(types, handlerType)
336+ }
337+ return types
338+}
339+
315340 // dispatchResponse serializes the payload and invokes a window callback in JS.
316341 func (r *MessageRouter) dispatchResponse(ctx context.Context, webviewID WebViewID, callback, world string, payload any) error {
317342 if ctx == nil {
318- ctx = r.baseCtx
343+ ctx = r.baseContext()
319344 }
320345
321346 if callback == "" {
@@ -68,7 +68,22 @@ func (r *MessageRouter) SetBaseContext(ctx context.Context) {
68 if ctx == nil {68 if ctx == nil {
69 ctx = context.Background()69 ctx = context.Background()
70 }70 }
71+ r.mu.Lock()
71 r.baseCtx = ctx72 r.baseCtx = ctx
73+ r.mu.Unlock()
74+}
75+
76+func (r *MessageRouter) baseContext() context.Context {
77+ if r == nil {
78+ return context.Background()
79+ }
80+ r.mu.RLock()
81+ ctx := r.baseCtx
82+ r.mu.RUnlock()
83+ if ctx == nil {
84+ return context.Background()
85+ }
86+ return ctx
72 }87 }
73 88
74 // RegisterHandler registers a handler for a message type.89 // RegisterHandler registers a handler for a message type.
@@ -116,7 +131,8 @@ func (r *MessageRouter) RegisterHandlerWithCallbacks(msgType, callback, errorCal
116 // WebKit's messageHandlers is only available in main world.131 // WebKit's messageHandlers is only available in main world.
117 // The isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler.132 // The isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler.
118 func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ string) (uint, error) {133 func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ string) (uint, error) {
119- log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()134+ baseCtx := r.baseContext()
135+ log := logging.FromContext(baseCtx).With().Str("component", "message-router").Logger()
120 136
121 log.Debug().Msg("SetupMessageHandler called")137 log.Debug().Msg("SetupMessageHandler called")
122 138
@@ -166,7 +182,8 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st
166 182
167 // handleScriptMessage decodes the JSC value and routes it to the correct handler.183 // handleScriptMessage decodes the JSC value and routes it to the correct handler.
168 func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager, valuePtr uintptr) {184 func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager, valuePtr uintptr) {
169- log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()185+ baseCtx := r.baseContext()
186+ log := logging.FromContext(baseCtx).With().Str("component", "message-router").Logger()
170 187
171 log.Debug().Uint64("value_ptr", uint64(valuePtr)).Msg("handleScriptMessage entry")188 log.Debug().Uint64("value_ptr", uint64(valuePtr)).Msg("handleScriptMessage entry")
172 189
@@ -225,13 +242,11 @@ func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager,
225 242
226 entry, ok := r.getHandler(msg.Type)243 entry, ok := r.getHandler(msg.Type)
227 if !ok || entry.handler == nil {244 if !ok || entry.handler == nil {
228- log.Warn().Str("type", msg.Type).Int("registered_handlers", len(r.handlers)).Msg("no handler registered for message type")245+ handlerTypes := r.registeredHandlerTypes()
229- // Debug: list all registered handlers246+ log.Warn().Str("type", msg.Type).Int("registered_handlers", len(handlerTypes)).Msg("no handler registered for message type")
230- r.mu.RLock()247+ for _, handlerType := range handlerTypes {
231- for handlerType := range r.handlers {
232 log.Debug().Str("registered_type", handlerType).Msg("available handler")248 log.Debug().Str("registered_type", handlerType).Msg("available handler")
233 }249 }
234- r.mu.RUnlock()
235 return250 return
236 }251 }
237 252
@@ -242,12 +257,12 @@ func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager,
242 Str("callback", entry.callback).257 Str("callback", entry.callback).
243 Msg("received script message, dispatching to handler")258 Msg("received script message, dispatching to handler")
244 259
245- resp, err := entry.handler.Handle(r.baseCtx, WebViewID(msg.WebViewID), msg.Payload)260+ resp, err := entry.handler.Handle(baseCtx, WebViewID(msg.WebViewID), msg.Payload)
246 log.Debug().Str("type", msg.Type).Err(err).Msg("handler execution completed")261 log.Debug().Str("type", msg.Type).Err(err).Msg("handler execution completed")
247 if err != nil {262 if err != nil {
248 log.Error().Err(err).Str("type", msg.Type).Msg("message handler returned error")263 log.Error().Err(err).Str("type", msg.Type).Msg("message handler returned error")
249 if entry.errorCallback != "" {264 if entry.errorCallback != "" {
250- if respErr := r.dispatchResponse(r.baseCtx, WebViewID(msg.WebViewID), entry.errorCallback, entry.world, err.Error()); respErr != nil {265+ if respErr := r.dispatchResponse(baseCtx, WebViewID(msg.WebViewID), entry.errorCallback, entry.world, err.Error()); respErr != nil {
251 log.Warn().Err(respErr).Msg("failed to dispatch error callback")266 log.Warn().Err(respErr).Msg("failed to dispatch error callback")
252 }267 }
253 }268 }
@@ -255,7 +270,7 @@ func (r *MessageRouter) handleScriptMessage(senderUCM webkit.UserContentManager,
255 }270 }
256 271
257 if entry.callback != "" {272 if entry.callback != "" {
258- if err := r.dispatchResponse(r.baseCtx, WebViewID(msg.WebViewID), entry.callback, entry.world, resp); err != nil {273+ if err := r.dispatchResponse(baseCtx, WebViewID(msg.WebViewID), entry.callback, entry.world, resp); err != nil {
259 log.Warn().Err(err).274 log.Warn().Err(err).
260 Str("callback", entry.callback).275 Str("callback", entry.callback).
261 Uint64("webview_id", msg.WebViewID).276 Uint64("webview_id", msg.WebViewID).
@@ -291,7 +306,7 @@ func (r *MessageRouter) syncWebViewID(wv *WebView) {
291 return306 return
292 }307 }
293 script := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(wv.ID()))308 script := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(wv.ID()))
294- wv.RunJavaScriptInWorld(r.baseCtx, script, "")309+ wv.RunJavaScriptInWorld(r.baseContext(), script, "")
295 }310 }
296 311
297 func (r *MessageRouter) markWebViewIDSynced(id WebViewID) bool {312 func (r *MessageRouter) markWebViewIDSynced(id WebViewID) bool {
@@ -312,10 +327,20 @@ func (r *MessageRouter) getHandler(msgType string) (handlerEntry, bool) {
312 return entry, ok327 return entry, ok
313 }328 }
314 329
330+func (r *MessageRouter) registeredHandlerTypes() []string {
331+ r.mu.RLock()
332+ defer r.mu.RUnlock()
333+ types := make([]string, 0, len(r.handlers))
334+ for handlerType := range r.handlers {
335+ types = append(types, handlerType)
336+ }
337+ return types
338+}
339+
315 // dispatchResponse serializes the payload and invokes a window callback in JS.340 // dispatchResponse serializes the payload and invokes a window callback in JS.
316 func (r *MessageRouter) dispatchResponse(ctx context.Context, webviewID WebViewID, callback, world string, payload any) error {341 func (r *MessageRouter) dispatchResponse(ctx context.Context, webviewID WebViewID, callback, world string, payload any) error {
317 if ctx == nil {342 if ctx == nil {
318- ctx = r.baseCtx343+ ctx = r.baseContext()
319 }344 }
320 345
321 if callback == "" {346 if callback == "" {
M internal/infrastructure/webkit/messaging_test.go +29 -0
@@ -1,11 +1,40 @@
11 package webkit
22
33 import (
4+ "context"
5+ "sync"
46 "testing"
57
68 "github.com/stretchr/testify/require"
79 )
810
11+type messageRouterContextKey struct{}
12+
13+func TestMessageRouterBaseContextConcurrentUpdate(t *testing.T) {
14+ t.Parallel()
15+
16+ router := NewMessageRouter(context.Background())
17+ var wg sync.WaitGroup
18+ wg.Add(2)
19+
20+ go func() {
21+ defer wg.Done()
22+ for i := range 1000 {
23+ router.SetBaseContext(context.WithValue(context.Background(), messageRouterContextKey{}, i))
24+ }
25+ }()
26+ go func() {
27+ defer wg.Done()
28+ for range 1000 {
29+ if router.baseContext() == nil {
30+ t.Error("base context must never be nil")
31+ }
32+ }
33+ }()
34+
35+ wg.Wait()
36+}
37+
938 func TestIsTrustedBridgeURI(t *testing.T) {
1039 t.Parallel()
1140
@@ -1,11 +1,40 @@
1 package webkit1 package webkit
2 2
3 import (3 import (
4+ "context"
5+ "sync"
4 "testing"6 "testing"
5 7
6 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
7 )9 )
8 10
11+type messageRouterContextKey struct{}
12+
13+func TestMessageRouterBaseContextConcurrentUpdate(t *testing.T) {
14+ t.Parallel()
15+
16+ router := NewMessageRouter(context.Background())
17+ var wg sync.WaitGroup
18+ wg.Add(2)
19+
20+ go func() {
21+ defer wg.Done()
22+ for i := range 1000 {
23+ router.SetBaseContext(context.WithValue(context.Background(), messageRouterContextKey{}, i))
24+ }
25+ }()
26+ go func() {
27+ defer wg.Done()
28+ for range 1000 {
29+ if router.baseContext() == nil {
30+ t.Error("base context must never be nil")
31+ }
32+ }
33+ }()
34+
35+ wg.Wait()
36+}
37+
9 func TestIsTrustedBridgeURI(t *testing.T) {38 func TestIsTrustedBridgeURI(t *testing.T) {
10 t.Parallel()39 t.Parallel()
11 40
M internal/infrastructure/webkit/scheme_handler.go +1 -1
@@ -499,7 +499,7 @@ func safeSystemviewsAssetPath(assetDir, relPath string) (fullPath, cleanRelPath
499499 return fullPath, cleanRelPath, true
500500 }
501501
502-func readAssetWithEncoding(assets embed.FS, fullPath, relPath string) ([]byte, error) {
502+func readAssetWithEncoding(assets fs.FS, fullPath, relPath string) ([]byte, error) {
503503 var compressedErr error
504504 if strings.HasSuffix(relPath, ".wasm") {
505505 if compressed, err := fs.ReadFile(assets, fullPath+".br"); err == nil {
@@ -499,7 +499,7 @@ func safeSystemviewsAssetPath(assetDir, relPath string) (fullPath, cleanRelPath
499 return fullPath, cleanRelPath, true499 return fullPath, cleanRelPath, true
500 }500 }
501 501
502-func readAssetWithEncoding(assets embed.FS, fullPath, relPath string) ([]byte, error) {502+func readAssetWithEncoding(assets fs.FS, fullPath, relPath string) ([]byte, error) {
503 var compressedErr error503 var compressedErr error
504 if strings.HasSuffix(relPath, ".wasm") {504 if strings.HasSuffix(relPath, ".wasm") {
505 if compressed, err := fs.ReadFile(assets, fullPath+".br"); err == nil {505 if compressed, err := fs.ReadFile(assets, fullPath+".br"); err == nil {
M internal/infrastructure/webkit/scheme_handler_test.go +21 -4
@@ -1,12 +1,15 @@
11 package webkit
22
33 import (
4+ "bytes"
45 "context"
56 "net/http"
67 "net/url"
78 "strings"
89 "testing"
10+ "testing/fstest"
911
12+ "github.com/andybalholm/brotli"
1013 "github.com/bnema/dumber/assets"
1114 "github.com/stretchr/testify/assert"
1215 "github.com/stretchr/testify/require"
@@ -327,13 +330,27 @@ func TestResponseHeadersForPath_WASMIncludesContentTypeAndCORS(t *testing.T) {
327330 assert.Equal(t, "GET, POST, OPTIONS", headers["Access-Control-Allow-Methods"])
328331 }
329332
330-func TestReadAssetWithEncodingDecompressesEmbeddedBrotliWASM(t *testing.T) {
333+func TestReadAssetWithEncodingDecompressesBrotliWASM(t *testing.T) {
331334 t.Parallel()
332335
333- data, err := readAssetWithEncoding(assets.WebUIAssets, "systemviews/systemviews.wasm", "systemviews.wasm")
336+ wasm := []byte("\x00asmfixture")
337+ fsys := fstest.MapFS{
338+ "systemviews/systemviews.wasm.br": {Data: brotliCompressForTest(t, wasm)},
339+ }
340+
341+ data, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
342+ require.NoError(t, err)
343+ assert.Equal(t, wasm, data)
344+}
345+
346+func brotliCompressForTest(t *testing.T, data []byte) []byte {
347+ t.Helper()
348+ var buf bytes.Buffer
349+ w := brotli.NewWriter(&buf)
350+ _, err := w.Write(data)
334351 require.NoError(t, err)
335- require.GreaterOrEqual(t, len(data), 4)
336- assert.Equal(t, "\x00asm", string(data[:4]))
352+ require.NoError(t, w.Close())
353+ return buf.Bytes()
337354 }
338355
339356 func TestHandleAssetRejectsTraversalOutsideSystemviews(t *testing.T) {
@@ -1,12 +1,15 @@
1 package webkit1 package webkit
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
5 "net/http"6 "net/http"
6 "net/url"7 "net/url"
7 "strings"8 "strings"
8 "testing"9 "testing"
10+ "testing/fstest"
9 11
12+ "github.com/andybalholm/brotli"
10 "github.com/bnema/dumber/assets"13 "github.com/bnema/dumber/assets"
11 "github.com/stretchr/testify/assert"14 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
@@ -327,13 +330,27 @@ func TestResponseHeadersForPath_WASMIncludesContentTypeAndCORS(t *testing.T) {
327 assert.Equal(t, "GET, POST, OPTIONS", headers["Access-Control-Allow-Methods"])330 assert.Equal(t, "GET, POST, OPTIONS", headers["Access-Control-Allow-Methods"])
328 }331 }
329 332
330-func TestReadAssetWithEncodingDecompressesEmbeddedBrotliWASM(t *testing.T) {333+func TestReadAssetWithEncodingDecompressesBrotliWASM(t *testing.T) {
331 t.Parallel()334 t.Parallel()
332 335
333- data, err := readAssetWithEncoding(assets.WebUIAssets, "systemviews/systemviews.wasm", "systemviews.wasm")336+ wasm := []byte("\x00asmfixture")
337+ fsys := fstest.MapFS{
338+ "systemviews/systemviews.wasm.br": {Data: brotliCompressForTest(t, wasm)},
339+ }
340+
341+ data, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
342+ require.NoError(t, err)
343+ assert.Equal(t, wasm, data)
344+}
345+
346+func brotliCompressForTest(t *testing.T, data []byte) []byte {
347+ t.Helper()
348+ var buf bytes.Buffer
349+ w := brotli.NewWriter(&buf)
350+ _, err := w.Write(data)
334 require.NoError(t, err)351 require.NoError(t, err)
335- require.GreaterOrEqual(t, len(data), 4)352+ require.NoError(t, w.Close())
336- assert.Equal(t, "\x00asm", string(data[:4]))353+ return buf.Bytes()
337 }354 }
338 355
339 func TestHandleAssetRejectsTraversalOutsideSystemviews(t *testing.T) {356 func TestHandleAssetRejectsTraversalOutsideSystemviews(t *testing.T) {

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