From 9e9d03d34c5b6aebf14dbe215ea2510daa8f6a18 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 16 Jun 2026 11:09:51 +0000 Subject: [PATCH 1/2] fix(wifi): do not run onError after successful connect When connectToWifi succeeded but onSuccess was omitted, _handleWifiResult still invoked onError because the error branch had no guard on result. IoT provisioning flows that only define onError would navigate to failure screens despite a successful WiFi join. Co-authored-by: Sharjeel Yunus --- modules/ensemble/lib/action/wifi_action.dart | 9 +++-- modules/ensemble/test/wifi_action_test.dart | 39 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 modules/ensemble/test/wifi_action_test.dart diff --git a/modules/ensemble/lib/action/wifi_action.dart b/modules/ensemble/lib/action/wifi_action.dart index c46b20237..b6e8608f7 100644 --- a/modules/ensemble/lib/action/wifi_action.dart +++ b/modules/ensemble/lib/action/wifi_action.dart @@ -19,9 +19,12 @@ Future _handleWifiResult( EnsembleAction? onError, bool? result, ) { - if (result == true && onSuccess != null) { - return ScreenController().executeAction(context, onSuccess, - event: EnsembleEvent(initiator, data: {'connected': true})); + if (result == true) { + if (onSuccess != null) { + return ScreenController().executeAction(context, onSuccess, + event: EnsembleEvent(initiator, data: {'connected': true})); + } + return Future.value(null); } if (onError != null) { final message = result == null diff --git a/modules/ensemble/test/wifi_action_test.dart b/modules/ensemble/test/wifi_action_test.dart new file mode 100644 index 000000000..6eb71c146 --- /dev/null +++ b/modules/ensemble/test/wifi_action_test.dart @@ -0,0 +1,39 @@ +import 'package:ensemble/action/wifi_action.dart'; +import 'package:ensemble/framework/action.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yaml/yaml.dart'; + +void main() { + group('ConnectToWifiAction.fromYaml', () { + test('parses connect operation by default', () { + final action = ConnectToWifiAction.fromYaml(payload: { + 'ssid': 'MyNetwork', + 'password': 'secret', + }); + + expect(action.operation, WifiOperation.connect); + expect(action.ssid, 'MyNetwork'); + expect(action.password, 'secret'); + }); + + test('parses disconnect operation', () { + final action = ConnectToWifiAction.fromYaml(payload: { + 'operation': 'disconnect', + }); + + expect(action.operation, WifiOperation.disconnect); + }); + + test('parses optional callbacks', () { + final action = ConnectToWifiAction.fromYaml(payload: { + 'ssid': 'MyNetwork', + 'onError': { + 'showToast': {'message': 'failed'}, + }, + }); + + expect(action.onSuccess, isNull); + expect(action.onError, isA()); + }); + }); +} From 64380e90f3bd70fe3fc30cdaab102144ec93bb6a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 16 Jun 2026 11:09:55 +0000 Subject: [PATCH 2/2] fix(action): await executeAction body before apiMap restore The finally block in ExecuteActionAction ran as soon as the inner Future was returned, not when async work finished. Scoped APIs were removed before invokeAPI and other async body actions could run, causing crashes or wrong endpoints for reusable actions with async bodies. Co-authored-by: Sharjeel Yunus --- .../ensemble/lib/action/execute_action.dart | 2 +- .../test/action_scope_api_restore_test.dart | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/modules/ensemble/lib/action/execute_action.dart b/modules/ensemble/lib/action/execute_action.dart index 77983cb89..93bb168ff 100644 --- a/modules/ensemble/lib/action/execute_action.dart +++ b/modules/ensemble/lib/action/execute_action.dart @@ -100,7 +100,7 @@ class ExecuteActionAction extends EnsembleAction { "Action '$name' contains an invalid 'body' payload."); } - return ScreenController() + return await ScreenController() .executeActionWithScope(context, childScope, innerAction); } finally { ActionScopeUtil.restorePageApisAfterAction(scopeManager, apiSnapshot); diff --git a/modules/ensemble/test/action_scope_api_restore_test.dart b/modules/ensemble/test/action_scope_api_restore_test.dart index 2c80a5233..6678af104 100644 --- a/modules/ensemble/test/action_scope_api_restore_test.dart +++ b/modules/ensemble/test/action_scope_api_restore_test.dart @@ -96,4 +96,41 @@ void main() { expect(scopeManager.pageData.apiMap!['sharedApi'], same(pageApi)); }); }); + + group('executeAction async restore timing', () { + Future> runWithFinally({required bool awaitInner}) async { + final order = []; + + Future inner() async { + await Future.delayed(const Duration(milliseconds: 20)); + order.add('inner'); + } + + try { + if (awaitInner) { + await inner(); + } else { + // Mirrors ExecuteActionAction returning executeActionWithScope + // without await: finally runs before the inner Future completes. + // ignore: unawaited_futures + inner(); + } + } finally { + order.add('restore'); + } + return order; + } + + test('restore runs before inner work without await', () async { + final order = await runWithFinally(awaitInner: false); + await Future.delayed(const Duration(milliseconds: 30)); + expect(order.first, 'restore'); + expect(order.last, 'inner'); + }); + + test('restore runs after inner work with await', () async { + final order = await runWithFinally(awaitInner: true); + expect(order, ['inner', 'restore']); + }); + }); }