From c44e2e6f51b17b6823f286367160e19e506e63ac Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Tue, 14 Jul 2026 15:47:18 +0200 Subject: [PATCH 1/7] Promote missing record fields from warning to compile error During the #243 migration a TextConfig construction missing the new tcTextColor field compiled with only the default -Wmissing-fields warning, which was overlooked in the build output, and crashed at runtime inside the test suite (in errorWidget of all places). A missing field is always a latent runtime crash, so the warning that can be missed becomes an error that cannot. Co-Authored-By: Claude Fable 5 --- hatter.cabal | 1 + 1 file changed, 1 insertion(+) diff --git a/hatter.cabal b/hatter.cabal index a781c20..09fe483 100644 --- a/hatter.cabal +++ b/hatter.cabal @@ -76,6 +76,7 @@ common common-options -O2 -Wall -split-sections -Wincomplete-uni-patterns -Wincomplete-record-updates -Widentities -Wredundant-constraints -Wcpp-undef -fwarn-tabs -Wpartial-fields + -Werror=missing-fields -fdefer-diagnostics -Wunused-packages -fenable-th-splice-warnings -fno-omit-yields From 0e5c661ad4e7b8d7d9ee2bc8c4dbe8dd2aa61ec0 Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Tue, 14 Jul 2026 16:19:58 +0200 Subject: [PATCH 2/7] Complete the three UserState constructions the new flag caught -Werror=missing-fields immediately found three long-standing incomplete UserState constructions in the tests (two in AppContextTests, one in WidgetTests), each missing userPlatformSignInState and userRequestRedraw and surviving only because nothing evaluated those fields. They get a fresh sign-in state and a no-op redraw. Also documents the flag in the changelog. The first push missed these because the local incremental build did not recompile the test modules under the new flag; verified with a forced full recompile this time. Co-Authored-By: Claude Fable 5 --- Changelog.md | 7 +++++++ test/Test/AppContextTests.hs | 7 +++++++ test/Test/WidgetTests.hs | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/Changelog.md b/Changelog.md index 7d4671a..e97aeaf 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,13 @@ ### Changed +- Missing fields in record construction are compile errors now + (`-Werror=missing-fields`): the default-on warning was overlooked + during the #243 migration and surfaced as a runtime crash in the + test suite, and the same flag immediately caught three long-standing + incomplete `UserState` constructions in the tests. A missing field + is always a latent runtime crash, so the build refuses it outright. + - BREAKING: text color moved from `WidgetStyle` into the text-bearing configs (`tcTextColor` / `bcTextColor` / `tiTextColor`, next to the font override), following the `tcFontConfig` precedent. A text diff --git a/test/Test/AppContextTests.hs b/test/Test/AppContextTests.hs index 45edf6d..3e78073 100644 --- a/test/Test/AppContextTests.hs +++ b/test/Test/AppContextTests.hs @@ -32,6 +32,7 @@ import Hatter.Lifecycle ) import Hatter.Widget (ButtonConfig(..), TextConfig(..), Widget(..)) import Hatter.Permission (newPermissionState) +import Hatter.PlatformSignIn (newPlatformSignInState) import Hatter.SecureStorage (newSecureStorageState) import Hatter.Ble (newBleState) import Hatter.Dialog (newDialogState) @@ -76,6 +77,7 @@ registrationTests = testGroup "Registration" dummyHttpState <- newHttpState dummyNetworkStatusState <- newNetworkStatusState dummyAnimationState <- newAnimationState + dummyPlatformSignInState <- newPlatformSignInState let dummyUserState = UserState { userPermissionState = dummyPermState , userSecureStorageState = dummySecureStorageState @@ -88,6 +90,8 @@ registrationTests = testGroup "Registration" , userHttpState = dummyHttpState , userNetworkStatusState = dummyNetworkStatusState , userAnimationState = dummyAnimationState + , userPlatformSignInState = dummyPlatformSignInState + , userRequestRedraw = pure () } viewFn <- readIORef (acViewFunction appCtx) widget <- viewFn dummyUserState @@ -124,6 +128,7 @@ registrationTests = testGroup "Registration" dummyHttpState <- newHttpState dummyNetworkStatusState <- newNetworkStatusState dummyAnimationState <- newAnimationState + dummyPlatformSignInState <- newPlatformSignInState let dummyUserState = UserState { userPermissionState = dummyPermState , userSecureStorageState = dummySecureStorageState @@ -136,6 +141,8 @@ registrationTests = testGroup "Registration" , userHttpState = dummyHttpState , userNetworkStatusState = dummyNetworkStatusState , userAnimationState = dummyAnimationState + , userPlatformSignInState = dummyPlatformSignInState + , userRequestRedraw = pure () } viewFnA <- readIORef (acViewFunction appCtxA) viewFnB <- readIORef (acViewFunction appCtxB) diff --git a/test/Test/WidgetTests.hs b/test/Test/WidgetTests.hs index b5cf06b..a00a6f8 100644 --- a/test/Test/WidgetTests.hs +++ b/test/Test/WidgetTests.hs @@ -56,6 +56,7 @@ import Hatter.Widget ) import Hatter.Render (RenderState(..), RenderedNode(..), renderWidget, dispatchEvent, dispatchTextEvent) import Hatter.Permission (newPermissionState) +import Hatter.PlatformSignIn (newPlatformSignInState) import Hatter.SecureStorage (newSecureStorageState) import Hatter.Ble (newBleState) import Hatter.Dialog (newDialogState) @@ -160,6 +161,7 @@ uiTests = testGroup "UI" dummyHttpState <- newHttpState dummyNetworkStatusState <- newNetworkStatusState dummyAnimationState <- newAnimationState + dummyPlatformSignInState <- newPlatformSignInState let dummyUserState = UserState { userPermissionState = dummyPermState , userSecureStorageState = dummySecureStorageState @@ -172,6 +174,8 @@ uiTests = testGroup "UI" , userHttpState = dummyHttpState , userNetworkStatusState = dummyNetworkStatusState , userAnimationState = dummyAnimationState + , userPlatformSignInState = dummyPlatformSignInState + , userRequestRedraw = pure () } app <- testApp widget <- maView app dummyUserState From 585f6b910588ff782c7787d6f91e55952384fe93 Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Tue, 14 Jul 2026 17:12:38 +0200 Subject: [PATCH 3/7] ci: give the android job 70 minutes for flag-forced cold rebuilds The -Werror=missing-fields change rebuilds every Haskell module, and a timeout-cancelled job skips its nix cache save, so retrying at 45 minutes would start cold every time (run 29340371964 died at exactly 45 minutes mid emulator test). Warm runs stay ~25 minutes. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6847ed7..187c61d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -104,7 +104,13 @@ jobs: android: runs-on: ubuntu-latest - timeout-minutes: 45 + # 70 rather than 45: a ghc-options change rebuilds every Haskell + # module and the nix store cache is only saved by a COMPLETED job + # (a timeout-cancel skips the save), so a cold rebuild has to fit + # in one run or every retry starts cold again; run 29340371964 + # timed out at exactly 45 minutes mid emulator test after such a + # rebuild. Warm runs take ~25 minutes. + timeout-minutes: 70 steps: - name: Free disk space run: | From 0c5ad55481478da9d0fa9d0e38cd2d04b8302be0 Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Tue, 14 Jul 2026 17:17:01 +0200 Subject: [PATCH 4/7] ci: correct the android timeout comment, the job never saves a cache Review finding: the android job has no cache-save step (the only cache-nix-action in the workflow belongs to macos-apk); it reads nix-cache.jappie.me and pushes nothing, so run completion has no effect on the next run's warmth. The real reason for 70 minutes is that a ghc-options change invalidates every Haskell derivation and each run builds cold until the external cache is populated. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 187c61d..cd60970 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -104,12 +104,13 @@ jobs: android: runs-on: ubuntu-latest - # 70 rather than 45: a ghc-options change rebuilds every Haskell - # module and the nix store cache is only saved by a COMPLETED job - # (a timeout-cancel skips the save), so a cold rebuild has to fit - # in one run or every retry starts cold again; run 29340371964 - # timed out at exactly 45 minutes mid emulator test after such a - # rebuild. Warm runs take ~25 minutes. + # 70 rather than 45: a ghc-options change invalidates every + # Haskell derivation, and this job only READS nix-cache.jappie.me + # (extra-substituters; nothing here pushes to it), so each run of + # such a change builds cold until the cache is populated + # externally, and the budget must fit a full cold build; run + # 29340371964 timed out at exactly 45 minutes mid emulator test + # on such a build. Warm runs take ~25 minutes. timeout-minutes: 70 steps: - name: Free disk space From 69549e6f28b488704e11a9eefcf31a1cdc43a310 Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Tue, 14 Jul 2026 18:12:10 +0200 Subject: [PATCH 5/7] ci: give the ios job 70 minutes for cold rebuilds plus the simulator test Same arithmetic as the android bump: the job reads nix-cache.jappie.me and pushes nothing, so ghc-options changes build cold until the cache is fed externally, and the simulator test (the known #235 flake) then runs out of clock at 45 minutes; two runs on this PR lost it to the cutoff. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cd60970..fdfe8ad 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -164,7 +164,13 @@ jobs: ios: runs-on: macos-latest - timeout-minutes: 45 + # 70 rather than 45: like the android job, this only READS + # nix-cache.jappie.me, so a ghc-options change builds cold until + # the cache is fed externally, and the leftover budget must still + # fit the simulator test (which is also the #235 flake); runs + # 29334286560 and 29344658446 both lost the simulator test to the + # 45-minute cutoff after cold-ish builds. + timeout-minutes: 70 steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v31 From 045273e5bce334288cf69bca8e749dcb675f3ead Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Tue, 14 Jul 2026 18:15:29 +0200 Subject: [PATCH 6/7] ci: cite the right runs in the ios timeout comment Review finding: run 29334286560 passed (44m17s, on #243) and was miscited as a timeout casualty. The actual prior instance of the build-passes-simulator-test-cancelled fingerprint is run 29281533228 on #239, alongside this PR's run 29344658446 and the historical mid-compile timeout 9dcf105. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fdfe8ad..b5b851d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -167,9 +167,10 @@ jobs: # 70 rather than 45: like the android job, this only READS # nix-cache.jappie.me, so a ghc-options change builds cold until # the cache is fed externally, and the leftover budget must still - # fit the simulator test (which is also the #235 flake); runs - # 29334286560 and 29344658446 both lost the simulator test to the - # 45-minute cutoff after cold-ish builds. + # fit the simulator test (which is also the #235 flake). The + # cutoff has eaten the simulator test after a successful build + # twice (run 29281533228 on #239, run 29344658446 on #244) and + # once mid-compile before that (9dcf105). timeout-minutes: 70 steps: - uses: actions/checkout@v4 From 093c51f491e0368e6453992586c07b83d8d552b3 Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Tue, 14 Jul 2026 19:08:44 +0200 Subject: [PATCH 7/7] ci: retrigger after a macOS runner's nix daemon broke watchos died in 44 seconds on 'opening lock file /nix/var/nix/db/big-lock: Permission denied'; the same tree passed watchos twice earlier today. Co-Authored-By: Claude Fable 5