Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions modules/ensemble/lib/framework/action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,8 @@ class NavigateViewGroupAction extends EnsembleAction {
} else if (viewIndex != null) {
final pageGroup = context.findAncestorWidgetOfExactType<PageGroup>();
final menuLen = pageGroup?.menu.menuItems.length ?? 0;
final resolvedIndex = menuLen > 0
? safeViewGroupPayloadIndex(viewIndex, menuLen)
: viewIndex;
final resolvedIndex =
resolveNavigateViewGroupTabIndex(viewIndex, menuLen);
if (payload != null) {
// TODO: this is wrong. Can't mutate the scope like this
scopeManager.dataContext.addDataContext(payload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ class CdnDefinitionProvider extends DefinitionProvider {
}
}

/// After a stale CDN fetch updates [_artifactCache], either apply changes
/// immediately (artifact refresh on + app initialized) or defer to resume.
Future<void> _applyStaleRefreshOutcome() async {
if (isArtifactRefreshEnabled() && Ensemble().getConfig() != null) {
await _handlePendingUpdate();
} else {
_hasPendingUpdate = true;
}
}

/// Handles pending CDN updates when app resumes.
/// CRITICAL: Must update appBundle and translations BEFORE firing refresh event
/// to ensure screens rebuild with the new resources (fixes race condition).
Expand Down Expand Up @@ -513,16 +523,7 @@ class CdnDefinitionProvider extends DefinitionProvider {
// Save to persistent cache
await _saveCachedState(jsonString);

// If artifact refresh is enabled and app is already initialized,
// immediately update appBundle and fire refresh event.
// This handles the cold start scenario where background refresh
// completes after initial render.
if (isArtifactRefreshEnabled() && Ensemble().getConfig() != null) {
await _handlePendingUpdate();
} else {
// Mark for later refresh on next resume
_hasPendingUpdate = true;
}
await _applyStaleRefreshOutcome();
} catch (e) {
if (kDebugMode) {
debugPrint('⚠️ CDN Provider: Refresh failed: $e');
Expand Down Expand Up @@ -925,6 +926,9 @@ class CdnDefinitionProvider extends DefinitionProvider {
@visibleForTesting
Future<void> handlePendingUpdateForTesting() => _handlePendingUpdate();

@visibleForTesting
Future<void> applyStaleRefreshOutcomeForTesting() => _applyStaleRefreshOutcome();

@visibleForTesting
bool get hasPendingUpdateForTesting => _hasPendingUpdate;

Expand Down
8 changes: 8 additions & 0 deletions modules/ensemble/lib/framework/view/page_group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,14 @@ int safeViewGroupPayloadIndex(int index, int payloadLength) {
return index;
}

/// Resolves a [NavigateViewGroupAction] tab index before [PageController.jumpToPage].
/// When [menuLength] is unknown (0), the raw index is preserved for later clamping.
@visibleForTesting
int resolveNavigateViewGroupTabIndex(int viewIndex, int menuLength) =>
menuLength > 0
? safeViewGroupPayloadIndex(viewIndex, menuLength)
: viewIndex;

class ViewGroupNotifier extends ChangeNotifier {
int _viewIndex = 0;
Map<String, dynamic>? _payload;
Expand Down
49 changes: 49 additions & 0 deletions modules/ensemble/test/cdn_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,55 @@ void main() {
});
});

group('CDN stale refresh outcome', () {
test('applies updates immediately when artifact refresh is enabled',
() async {
final provider = CdnDefinitionProvider('test-app');
final config = EnsembleConfig(definitionProvider: provider);
Ensemble().setEnsembleConfig(config);

await provider.applyRuntimeManifestForTesting(
_manifestWithArtifactRefresh(_manifestWithResourceVersion('v1')),
);
await config.updateAppBundle();

provider.rebuildManifestCacheForTesting(
_manifestWithArtifactRefresh(_manifestWithResourceVersion('v2')),
);

await provider.applyStaleRefreshOutcomeForTesting();

expect(provider.hasPendingUpdateForTesting, isFalse);
expect(
config.getResources()?[ResourceArtifactEntry.Scripts.name]['version'],
'v2',
);
});

test('defers updates when artifact refresh is disabled', () async {
final provider = CdnDefinitionProvider('test-app');
final config = EnsembleConfig(definitionProvider: provider);
Ensemble().setEnsembleConfig(config);

await provider.applyRuntimeManifestForTesting(
_manifestWithResourceVersion('v1'),
);
await config.updateAppBundle();

provider.rebuildManifestCacheForTesting(
_manifestWithResourceVersion('v2'),
);

await provider.applyStaleRefreshOutcomeForTesting();

expect(provider.hasPendingUpdateForTesting, isTrue);
expect(
config.getResources()?[ResourceArtifactEntry.Scripts.name]['version'],
'v1',
);
});
});

group('CDN pending update ordering', () {
test('handlePendingUpdate syncs app bundle from CDN cache and fires refresh',
() async {
Expand Down
12 changes: 12 additions & 0 deletions modules/ensemble/test/safe_view_group_payload_index_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import 'package:ensemble/framework/view/page_group.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('resolveNavigateViewGroupTabIndex', () {
test('clamps when menu length is known', () {
expect(resolveNavigateViewGroupTabIndex(5, 3), 2);
expect(resolveNavigateViewGroupTabIndex(-1, 2), 0);
});

test('preserves raw index when menu length is unknown', () {
expect(resolveNavigateViewGroupTabIndex(5, 0), 5);
expect(resolveNavigateViewGroupTabIndex(-1, 0), -1);
});
});

group('safeViewGroupPayloadIndex', () {
test('clamps to valid range for positive lengths', () {
expect(safeViewGroupPayloadIndex(-1, 3), 0);
Expand Down
Loading