


























@@ -202,6 +202,7 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
202202var status: NotificationAuthorizationStatus = .notDetermined
203203var requestAuthorizationResult = false
204204var requestAuthorizationCalls = 0
205+var addCalls = 0
205206206207func authorizationStatus() async -> NotificationAuthorizationStatus {
207208self.status
@@ -217,7 +218,9 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
217218return self.requestAuthorizationResult
218219}
219220220-func add(_: UNNotificationRequest) async throws {}
221+func add(_: UNNotificationRequest) async throws {
222+self.addCalls += 1
223+}
221224222225func removePendingNotificationRequests(withIdentifiers _: [String]) async {}
223226@@ -1230,13 +1233,76 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
12301233 hasStoredOperatorToken: false))
12311234}
123212351233-@Test @MainActor func successfulBootstrapOnboardingRequestsNotificationAuthorization() async {
1236+@Test @MainActor func successfulBootstrapOnboardingDoesNotRequestNotificationAuthorization() async {
12341237let center = MockBootstrapNotificationCenter()
12351238let appModel = NodeAppModel(notificationCenter: center)
1236123912371240await appModel._test_handleSuccessfulBootstrapGatewayOnboarding()
123812411239- #expect(center.requestAuthorizationCalls == 1)
1242+ #expect(center.requestAuthorizationCalls == 0)
1243+}
1244+1245+@Test @MainActor func operatorGatewayRequestedEventShowsNotificationGuidanceWhenNotificationsOff() async throws {
1246+let center = MockBootstrapNotificationCenter()
1247+ center.status = .notDetermined
1248+let appModel = NodeAppModel(notificationCenter: center)
1249+ appModel._test_resetExecApprovalNotificationGuidanceSuppression()
1250+defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
1251+1252+await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
1253+ type: "event",
1254+ event: ExecApprovalNotificationBridge.requestedKind,
1255+ payload: AnyCodable(["id": "approval-notifications-off"]),
1256+ seq: nil,
1257+ stateversion: nil))
1258+1259+let prompt = try #require(appModel._test_pendingNotificationPermissionGuidancePrompt())
1260+ #expect(prompt.approvalId == "approval-notifications-off")
1261+ #expect(center.requestAuthorizationCalls == 0)
1262+}
1263+1264+@Test @MainActor func suppressedOperatorGatewayRequestedEventDoesNotShowNotificationGuidance() async {
1265+let center = MockBootstrapNotificationCenter()
1266+ center.status = .denied
1267+let appModel = NodeAppModel(notificationCenter: center)
1268+ appModel._test_resetExecApprovalNotificationGuidanceSuppression()
1269+defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
1270+ appModel.dismissNotificationPermissionGuidancePrompt(suppressFuture: true)
1271+1272+await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
1273+ type: "event",
1274+ event: ExecApprovalNotificationBridge.requestedKind,
1275+ payload: AnyCodable(["id": "approval-suppressed"]),
1276+ seq: nil,
1277+ stateversion: nil))
1278+1279+ #expect(appModel._test_pendingNotificationPermissionGuidancePrompt() == nil)
1280+ #expect(center.requestAuthorizationCalls == 0)
1281+}
1282+1283+@Test @MainActor func operatorGatewayResolvedEventClearsNotificationGuidancePrompt() async throws {
1284+let center = MockBootstrapNotificationCenter()
1285+ center.status = .denied
1286+let appModel = NodeAppModel(notificationCenter: center)
1287+ appModel._test_resetExecApprovalNotificationGuidanceSuppression()
1288+defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
1289+1290+await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
1291+ type: "event",
1292+ event: ExecApprovalNotificationBridge.requestedKind,
1293+ payload: AnyCodable(["id": "approval-guidance-resolved"]),
1294+ seq: nil,
1295+ stateversion: nil))
1296+ _ = try #require(appModel._test_pendingNotificationPermissionGuidancePrompt())
1297+1298+await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
1299+ type: "event",
1300+ event: ExecApprovalNotificationBridge.resolvedKind,
1301+ payload: AnyCodable(["id": "approval-guidance-resolved"]),
1302+ seq: nil,
1303+ stateversion: nil))
1304+1305+ #expect(appModel._test_pendingNotificationPermissionGuidancePrompt() == nil)
12401306}
1241130712421308@Test func clearingBootstrapTokenStripsReconnectConfigEvenWithoutPersistence() throws {
@@ -1298,6 +1364,82 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
12981364 #expect(res.error?.message.contains("CAMERA_DISABLED") == true)
12991365}
130013661367+@Test @MainActor func systemNotifyDoesNotRequestNotificationAuthorizationWhenOff() async throws {
1368+let center = MockBootstrapNotificationCenter()
1369+ center.status = .notDetermined
1370+let appModel = NodeAppModel(notificationCenter: center)
1371+let params = OpenClawSystemNotifyParams(title: "Approval", body: "Review request")
1372+let paramsData = try JSONEncoder().encode(params)
1373+let req = BridgeInvokeRequest(
1374+ id: "notify-off",
1375+ command: OpenClawSystemCommand.notify.rawValue,
1376+ paramsJSON: String(decoding: paramsData, as: UTF8.self))
1377+1378+let res = await appModel._test_handleInvoke(req)
1379+1380+ #expect(res.ok == false)
1381+ #expect(res.error?.code == .unavailable)
1382+ #expect(res.error?.message == "NOT_AUTHORIZED: notifications")
1383+ #expect(center.requestAuthorizationCalls == 0)
1384+ #expect(center.addCalls == 0)
1385+}
1386+1387+@Test @MainActor func systemNotifySchedulesWhenNotificationsAreAlreadyAllowed() async throws {
1388+let center = MockBootstrapNotificationCenter()
1389+ center.status = .authorized
1390+let appModel = NodeAppModel(notificationCenter: center)
1391+let params = OpenClawSystemNotifyParams(title: "Approval", body: "Review request")
1392+let paramsData = try JSONEncoder().encode(params)
1393+let req = BridgeInvokeRequest(
1394+ id: "notify-on",
1395+ command: OpenClawSystemCommand.notify.rawValue,
1396+ paramsJSON: String(decoding: paramsData, as: UTF8.self))
1397+1398+let res = await appModel._test_handleInvoke(req)
1399+1400+ #expect(res.ok)
1401+ #expect(center.requestAuthorizationCalls == 0)
1402+ #expect(center.addCalls == 1)
1403+}
1404+1405+@Test @MainActor func chatPushWithoutSpeechDoesNotRequestNotificationAuthorizationWhenOff() async throws {
1406+let center = MockBootstrapNotificationCenter()
1407+ center.status = .notDetermined
1408+let appModel = NodeAppModel(notificationCenter: center)
1409+let params = OpenClawChatPushParams(text: "Build finished", speak: false)
1410+let paramsData = try JSONEncoder().encode(params)
1411+let req = BridgeInvokeRequest(
1412+ id: "chat-push-off",
1413+ command: OpenClawChatCommand.push.rawValue,
1414+ paramsJSON: String(decoding: paramsData, as: UTF8.self))
1415+1416+let res = await appModel._test_handleInvoke(req)
1417+1418+ #expect(res.ok == false)
1419+ #expect(res.error?.code == .unavailable)
1420+ #expect(res.error?.message == "NOT_AUTHORIZED: notifications")
1421+ #expect(center.requestAuthorizationCalls == 0)
1422+ #expect(center.addCalls == 0)
1423+}
1424+1425+@Test @MainActor func chatPushSchedulesWhenNotificationsAreAlreadyAllowed() async throws {
1426+let center = MockBootstrapNotificationCenter()
1427+ center.status = .authorized
1428+let appModel = NodeAppModel(notificationCenter: center)
1429+let params = OpenClawChatPushParams(text: "Build finished", speak: false)
1430+let paramsData = try JSONEncoder().encode(params)
1431+let req = BridgeInvokeRequest(
1432+ id: "chat-push-on",
1433+ command: OpenClawChatCommand.push.rawValue,
1434+ paramsJSON: String(decoding: paramsData, as: UTF8.self))
1435+1436+let res = await appModel._test_handleInvoke(req)
1437+1438+ #expect(res.ok)
1439+ #expect(center.requestAuthorizationCalls == 0)
1440+ #expect(center.addCalls == 1)
1441+}
1442+13011443@Test @MainActor func handleInvokeRejectsInvalidScreenFormat() async {
13021444let appModel = NodeAppModel()
13031445let params = OpenClawScreenRecordParams(format: "gif")
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。