From 980b8622eb4689bcd68cbc5cda48ce3ba9b2ec4e Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 8 May 2026 08:10:33 +0900 Subject: [PATCH 1/9] fix(ci): restore Android lint compatibility AGP 8.7.3 lint cannot analyze Kotlin 2.3 metadata, which breaks the root build. Keep the tooling on major version 8 while moving AGP and the Gradle wrapper to the documented Kotlin 2.3-compatible line.\n\nCo-Authored-By: Claude Opus 4.7 --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1fbc541..967eca2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.7.3" +agp = "8.13.2" androidx-activity-compose = "1.10.1" firebase-messaging = "24.1.0" androidx-core-ktx = "1.15.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 62d4bd44e04da601f2a4155caaf86e682a899fdf Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 8 May 2026 08:20:48 +0900 Subject: [PATCH 2/9] test(ios): keep setup E2E aligned Handle the notification permission alert, follow the current setup labels, and expose the icon-only send action for UI automation.\n\nCo-Authored-By: Claude Opus 4.7 --- .../ios/DeeplineIOS/Sources/RootView.swift | 1 + .../DeeplineIOSUITests.swift | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/clients/ios/DeeplineIOS/Sources/RootView.swift b/clients/ios/DeeplineIOS/Sources/RootView.swift index 9b4632b..590a5c3 100644 --- a/clients/ios/DeeplineIOS/Sources/RootView.swift +++ b/clients/ios/DeeplineIOS/Sources/RootView.swift @@ -1093,6 +1093,7 @@ private struct ChatInputBar: View { .foregroundStyle(draft.isEmpty ? DeeplineTheme.onSurfaceVariant(colorScheme) : .white) } } + .accessibilityLabel(draft.isEmpty ? "Voice message" : "Send") } .padding(.horizontal, 10) .padding(.vertical, 8) diff --git a/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift b/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift index 39fafe0..8c59226 100644 --- a/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift +++ b/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift @@ -9,18 +9,27 @@ final class DeeplineIOSUITests: XCTestCase { let app = XCUIApplication() app.launchArguments.append("-resetDeeplineState") app.launchEnvironment["DEEPLINE_SERVER_URL"] = "http://localhost:9091" + addUIInterruptionMonitor(withDescription: "Notification permission") { alert in + guard alert.buttons.count > 0 else { return false } + alert.buttons.element(boundBy: 0).tap() + return true + } app.launch() + app.tap() - let setupButton = app.buttons["Set Up Local Identity"] + let setupButton = app.buttons["Get Started"] XCTAssertTrue(setupButton.waitForExistence(timeout: 10)) setupButton.tap() - let displayNameField = app.textFields["Display name"] + let displayNameField = app.textFields["Enter your name"] + if !displayNameField.waitForExistence(timeout: 3), setupButton.exists { + setupButton.tap() + } XCTAssertTrue(displayNameField.waitForExistence(timeout: 10)) displayNameField.tap() displayNameField.typeText("Codex Tester") - let deviceField = app.textFields["Device label"] + let deviceField = app.textFields["e.g. iPhone 15 Pro"] XCTAssertTrue(deviceField.waitForExistence(timeout: 10)) deviceField.tap() if let currentValue = deviceField.value as? String, !currentValue.isEmpty { @@ -31,16 +40,16 @@ final class DeeplineIOSUITests: XCTestCase { app.buttons["Create Identity"].tap() - let composer = app.textFields["Write a private note"] + let composer = app.textFields["Message"] if !composer.waitForExistence(timeout: 20) { let localNotes = app.staticTexts["Local Notes"] XCTAssertTrue(localNotes.waitForExistence(timeout: 20)) localNotes.tap() } XCTAssertTrue(composer.waitForExistence(timeout: 10)) - XCTAssertTrue(app.buttons["Send"].exists) composer.tap() composer.typeText("UITest secure note") + XCTAssertTrue(app.buttons["Send"].exists) app.buttons["Send"].tap() XCTAssertTrue(app.staticTexts["UITest secure note"].waitForExistence(timeout: 10)) } From d3454b6b2444272f677d6072a8b43214b29d3bc4 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 8 May 2026 08:24:32 +0900 Subject: [PATCH 3/9] fix(ios): support Xcode 15 state updates Avoid in-place mutations of actor-isolated ObservableObject dictionaries so the GitHub iOS CI runner on Xcode 15 can compile the app. Co-Authored-By: OpenAI --- .../DeeplineIOS/Sources/DeeplineAppModel.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift index dffe00c..5410ef0 100644 --- a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift +++ b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift @@ -174,7 +174,7 @@ final class DeeplineAppModel: ObservableObject { try await self.ensureReachableServerBaseURL() let envelopes = try await self.client.listMessages(baseURL: self.serverBaseURL, conversationId: conversationId) let parsed = self.parseMessages(currentUserId: userId, envelopes: envelopes) - self.messages[conversationId] = parsed + self.replaceMessages(parsed, for: conversationId) self.updateConversationPreview(conversationId: conversationId, preview: parsed.last?.body) } } @@ -242,7 +242,7 @@ final class DeeplineAppModel: ObservableObject { } let newMessage = parseMessage(currentUserId: currentUserId, envelope: envelope) - messages[conversationId] = existingMessages + [newMessage] + replaceMessages(existingMessages + [newMessage], for: conversationId) updateConversationPreview(conversationId: conversationId, preview: newMessage.body) } @@ -353,7 +353,7 @@ final class DeeplineAppModel: ObservableObject { func loadGroupMembers(conversationId: String) async { await performSilentTask { [self] in let page = try await self.client.listConversationMembers(baseURL: self.serverBaseURL, conversationId: conversationId) - self.groupMembers[conversationId] = page.members + self.replaceGroupMembers(page.members, for: conversationId) } } @@ -473,6 +473,18 @@ final class DeeplineAppModel: ObservableObject { messages[conversationId] ?? [] } + private func replaceMessages(_ newMessages: [DeeplineMessage], for conversationId: String) { + var updatedMessages = messages + updatedMessages[conversationId] = newMessages + messages = updatedMessages + } + + private func replaceGroupMembers(_ members: [GroupMember], for conversationId: String) { + var updatedGroupMembers = groupMembers + updatedGroupMembers[conversationId] = members + groupMembers = updatedGroupMembers + } + func primaryConversationId() -> String? { chats.first?.id } From e78de74759535f2d4bd733c521c67a5fc3f44241 Mon Sep 17 00:00:00 2001 From: Hyo Date: Fri, 8 May 2026 08:26:49 +0900 Subject: [PATCH 4/9] fix(ios): compile member labels on Xcode 15 Convert the user-id suffix to String before interpolation so Swift 5.9 on the CI runner can compile RootView. Co-Authored-By: OpenAI --- clients/ios/DeeplineIOS/Sources/RootView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/ios/DeeplineIOS/Sources/RootView.swift b/clients/ios/DeeplineIOS/Sources/RootView.swift index 590a5c3..a9dbc0c 100644 --- a/clients/ios/DeeplineIOS/Sources/RootView.swift +++ b/clients/ios/DeeplineIOS/Sources/RootView.swift @@ -1440,7 +1440,7 @@ private struct MemberRow: View { ) VStack(alignment: .leading, spacing: 4) { - Text("User \(member.userId.suffix(4))") + Text("User \(String(member.userId.suffix(4)))") .font(.subheadline.weight(.medium)) .foregroundStyle(DeeplineTheme.onSurface(colorScheme)) From 370bb318fdd5fd14aec37f72c64f2644db32b9d9 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 9 May 2026 08:43:44 +0900 Subject: [PATCH 5/9] fix(ios): avoid redundant message refreshes Skip replacing the message dictionary when polling returns the same message list, avoiding unnecessary SwiftUI refreshes while keeping the Xcode 15-safe assignment path. Co-Authored-By: Claude Opus 4.7 --- clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift index 5410ef0..9466cbc 100644 --- a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift +++ b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift @@ -474,6 +474,7 @@ final class DeeplineAppModel: ObservableObject { } private func replaceMessages(_ newMessages: [DeeplineMessage], for conversationId: String) { + guard messages[conversationId] != newMessages else { return } var updatedMessages = messages updatedMessages[conversationId] = newMessages messages = updatedMessages From d7cc4e7c36d7657bc2af1b58a1ea1749e9fe0884 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 9 May 2026 08:50:23 +0900 Subject: [PATCH 6/9] fix(ios): settle review feedback on E2E fix Avoid redundant group member refreshes and wait for the send action in the UI test so the build-restoration PR can complete the bot review loop cleanly. Co-Authored-By: Claude Opus 4.7 --- clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift | 1 + clients/ios/DeeplineIOS/Sources/DeeplineServerClient.swift | 2 +- clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift index 9466cbc..41a0ac0 100644 --- a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift +++ b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift @@ -481,6 +481,7 @@ final class DeeplineAppModel: ObservableObject { } private func replaceGroupMembers(_ members: [GroupMember], for conversationId: String) { + guard groupMembers[conversationId] != members else { return } var updatedGroupMembers = groupMembers updatedGroupMembers[conversationId] = members groupMembers = updatedGroupMembers diff --git a/clients/ios/DeeplineIOS/Sources/DeeplineServerClient.swift b/clients/ios/DeeplineIOS/Sources/DeeplineServerClient.swift index 79016dd..ab331b2 100644 --- a/clients/ios/DeeplineIOS/Sources/DeeplineServerClient.swift +++ b/clients/ios/DeeplineIOS/Sources/DeeplineServerClient.swift @@ -180,7 +180,7 @@ enum GroupRole: String, Codable { case MEMBER } -struct GroupMember: Codable, Identifiable { +struct GroupMember: Codable, Equatable, Identifiable { let userId: String let role: GroupRole let addedByUserId: String? diff --git a/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift b/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift index 8c59226..eb27596 100644 --- a/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift +++ b/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift @@ -49,8 +49,9 @@ final class DeeplineIOSUITests: XCTestCase { XCTAssertTrue(composer.waitForExistence(timeout: 10)) composer.tap() composer.typeText("UITest secure note") - XCTAssertTrue(app.buttons["Send"].exists) - app.buttons["Send"].tap() + let sendButton = app.buttons["Send"] + XCTAssertTrue(sendButton.waitForExistence(timeout: 10)) + sendButton.tap() XCTAssertTrue(app.staticTexts["UITest secure note"].waitForExistence(timeout: 10)) } } From 35ef91cdd51d3a86c2ee5804ebcd7a6d8f890661 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 9 May 2026 09:03:29 +0900 Subject: [PATCH 7/9] fix(ios): update model dictionaries directly Keep the equality guards from review feedback while avoiding full dictionary copies inside the state update helpers. Co-Authored-By: Claude Opus 4.7 --- .../ios/DeeplineIOS/Sources/DeeplineAppModel.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift index 41a0ac0..4c72454 100644 --- a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift +++ b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift @@ -475,16 +475,12 @@ final class DeeplineAppModel: ObservableObject { private func replaceMessages(_ newMessages: [DeeplineMessage], for conversationId: String) { guard messages[conversationId] != newMessages else { return } - var updatedMessages = messages - updatedMessages[conversationId] = newMessages - messages = updatedMessages + messages[conversationId] = newMessages } - private func replaceGroupMembers(_ members: [GroupMember], for conversationId: String) { - guard groupMembers[conversationId] != members else { return } - var updatedGroupMembers = groupMembers - updatedGroupMembers[conversationId] = members - groupMembers = updatedGroupMembers + private func replaceGroupMembers(_ newMembers: [GroupMember], for conversationId: String) { + guard groupMembers[conversationId] != newMembers else { return } + groupMembers[conversationId] = newMembers } func primaryConversationId() -> String? { From 3e30877a8666a24f40450086e09a6fbb91530863 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 9 May 2026 09:10:36 +0900 Subject: [PATCH 8/9] fix(ios): reduce setup test flake paths Handle notification permission prompts by intent, remove the setup double tap, and avoid nil-versus-empty refreshes in iOS model state helpers. Co-Authored-By: Claude Opus 4.7 --- .../DeeplineIOS/Sources/DeeplineAppModel.swift | 4 ++-- .../DeeplineIOSUITests/DeeplineIOSUITests.swift | 16 ++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift index 4c72454..5b8506b 100644 --- a/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift +++ b/clients/ios/DeeplineIOS/Sources/DeeplineAppModel.swift @@ -474,12 +474,12 @@ final class DeeplineAppModel: ObservableObject { } private func replaceMessages(_ newMessages: [DeeplineMessage], for conversationId: String) { - guard messages[conversationId] != newMessages else { return } + guard (messages[conversationId] ?? []) != newMessages else { return } messages[conversationId] = newMessages } private func replaceGroupMembers(_ newMembers: [GroupMember], for conversationId: String) { - guard groupMembers[conversationId] != newMembers else { return } + guard (groupMembers[conversationId] ?? []) != newMembers else { return } groupMembers[conversationId] = newMembers } diff --git a/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift b/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift index eb27596..abe8d4a 100644 --- a/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift +++ b/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift @@ -10,8 +10,15 @@ final class DeeplineIOSUITests: XCTestCase { app.launchArguments.append("-resetDeeplineState") app.launchEnvironment["DEEPLINE_SERVER_URL"] = "http://localhost:9091" addUIInterruptionMonitor(withDescription: "Notification permission") { alert in - guard alert.buttons.count > 0 else { return false } - alert.buttons.element(boundBy: 0).tap() + if alert.buttons["Allow"].exists { + alert.buttons["Allow"].tap() + } else if alert.buttons.count > 1 { + alert.buttons.element(boundBy: 1).tap() + } else if alert.buttons.count > 0 { + alert.buttons.element(boundBy: 0).tap() + } else { + return false + } return true } app.launch() @@ -22,10 +29,7 @@ final class DeeplineIOSUITests: XCTestCase { setupButton.tap() let displayNameField = app.textFields["Enter your name"] - if !displayNameField.waitForExistence(timeout: 3), setupButton.exists { - setupButton.tap() - } - XCTAssertTrue(displayNameField.waitForExistence(timeout: 10)) + XCTAssertTrue(displayNameField.waitForExistence(timeout: 15)) displayNameField.tap() displayNameField.typeText("Codex Tester") From 17425d282278319b43d9b7872d321f49a2d257a5 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sat, 9 May 2026 09:18:17 +0900 Subject: [PATCH 9/9] fix(ios): narrow notification test interruption Only handle notification permission alerts during the setup UI test so unexpected alerts remain visible to XCTest. Co-Authored-By: Claude Opus 4.7 --- clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift b/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift index abe8d4a..268c598 100644 --- a/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift +++ b/clients/ios/DeeplineIOSUITests/DeeplineIOSUITests.swift @@ -10,6 +10,8 @@ final class DeeplineIOSUITests: XCTestCase { app.launchArguments.append("-resetDeeplineState") app.launchEnvironment["DEEPLINE_SERVER_URL"] = "http://localhost:9091" addUIInterruptionMonitor(withDescription: "Notification permission") { alert in + let alertText = ([alert.label] + alert.staticTexts.allElementsBoundByIndex.map(\.label)).joined(separator: " ") + guard alertText.localizedCaseInsensitiveContains("notification") else { return false } if alert.buttons["Allow"].exists { alert.buttons["Allow"].tap() } else if alert.buttons.count > 1 {