From 1e1fb48f16ab16c515f296eff036eb4a146433f1 Mon Sep 17 00:00:00 2001 From: Adron Hall Date: Wed, 16 Sep 2026 12:50:50 -0700 Subject: [PATCH] fix(settings): persist the "System" theme choice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The theme picker mapped "system" to nil, and a synthesized Encodable omits a nil field with encodeIfPresent, so the PATCH body encoded to `{}`. `/api/user/update` applies `...(theme !== undefined && { theme })`, so the key never arrived, the stored theme was never changed, and the response returned the unchanged user — snapping the picker back. Send "system" verbatim instead. The route performs no validation on `theme` and the web persists the same literal string, so the value round-trips; RootView.preferredScheme already treats any non-light/dark value as "follow the OS", so no change was needed there. The comment claiming the server rejects "system" was wrong and is gone. Tests pin the wire format: theme "system" sends {"theme":"system"}, and a nil theme omits the key entirely (the silent no-op that caused this). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017bss5MgZa7Jvj2m9zdaUd1 --- InterlinedList/Views/SettingsView.swift | 4 +--- .../APIClientProfileTests.swift | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/InterlinedList/Views/SettingsView.swift b/InterlinedList/Views/SettingsView.swift index 7674484..d182d74 100644 --- a/InterlinedList/Views/SettingsView.swift +++ b/InterlinedList/Views/SettingsView.swift @@ -82,9 +82,7 @@ struct SettingsView: View { // Guard against spurious saves when syncFromUser sets the initial value on appear. let serverTheme = authState.user?.theme ?? "system" guard newValue != serverTheme else { return } - // "system" means no explicit preference — send nil so the server clears it. - // Sending the string "system" is rejected or treated as default (light) by the server. - Task { await save(theme: newValue == "system" ? nil : newValue) } + Task { await save(theme: newValue) } } } } diff --git a/InterlinedListTests/APIClientTests/APIClientProfileTests.swift b/InterlinedListTests/APIClientTests/APIClientProfileTests.swift index 75b3c27..04fb563 100644 --- a/InterlinedListTests/APIClientTests/APIClientProfileTests.swift +++ b/InterlinedListTests/APIClientTests/APIClientProfileTests.swift @@ -85,4 +85,25 @@ final class APIClientProfileTests: XCTestCase { let user = try await sut.updateUserSettings(theme: "light") XCTAssertEqual(user.username, "alice") } + + func test_updateUserSettings_systemTheme_sendsLiteralSystemString() async throws { + session.stub(json: #"{"user":\#(userJSON)}"#) + _ = try await sut.updateUserSettings(theme: "system") + let body = try XCTUnwrap(session.lastRequest?.httpBody) + let json = try XCTUnwrap(try? JSONSerialization.jsonObject(with: body) as? [String: Any]) + XCTAssertEqual(json["theme"] as? String, "system", #"Body must be {"theme":"system"}"#) + XCTAssertEqual(json.count, 1, "Only the changed field belongs in the body") + } + + func test_updateUserSettings_nilTheme_omitsThemeKey() async throws { + // Synthesized Encodable uses encodeIfPresent, and /api/user/update applies + // `...(theme !== undefined && { theme })` — so a nil theme is a silent no-op, + // never a clear. Callers must send "system" verbatim to select the OS theme. + session.stub(json: #"{"user":\#(userJSON)}"#) + _ = try await sut.updateUserSettings(theme: nil, showAdvancedPostSettings: true) + let body = try XCTUnwrap(session.lastRequest?.httpBody) + let json = try XCTUnwrap(try? JSONSerialization.jsonObject(with: body) as? [String: Any]) + XCTAssertNil(json["theme"]) + XCTAssertEqual(json.count, 1) + } }