From 5b0a01a02a82fd6b2c6e043f67330ac77b364f7f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Jun 2026 11:07:11 +0000 Subject: [PATCH] fix(cdn): reject manifests missing artifacts before cache wipe Background CDN refresh called _rebuildFromRoot() which cleared all in-memory definitions before checking for an artifacts section. A transient or bad deploy returning parseable JSON without artifacts (e.g. {}) wiped the working session and could persist the unusable manifest via _saveCachedState. Validate the artifacts section exists before mutating cache state so failed refreshes keep the last good definitions. Co-authored-by: Sharjeel Yunus --- .../definition_providers/cdn_provider.dart | 11 +++++++++-- modules/ensemble/test/cdn_provider_test.dart | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart b/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart index c55536fb7..7f9d43850 100644 --- a/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart +++ b/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart @@ -720,6 +720,14 @@ class CdnDefinitionProvider extends DefinitionProvider { // -------------------------------------------------------- void _rebuildFromRoot(Map root) { + // Validate before mutating in-memory or persisted cache. A background + // refresh that clears first and then bails on missing artifacts would + // otherwise wipe a working session and persist an unusable manifest. + if (_asMap(root['artifacts']) == null) { + throw ConfigError( + 'CDN manifest is missing a valid artifacts section.'); + } + // reset caches/state _artifactCache.clear(); _screenNameMappings.clear(); @@ -729,8 +737,7 @@ class CdnDefinitionProvider extends DefinitionProvider { _appConfig = null; _applySecretsFromRoot(root); - final artifacts = _asMap(root['artifacts']); - if (artifacts == null) return; + final artifacts = _asMap(root['artifacts'])!; // 1) config final configMap = _asMap(artifacts['config']); diff --git a/modules/ensemble/test/cdn_provider_test.dart b/modules/ensemble/test/cdn_provider_test.dart index 3b855f166..f859bbcbc 100644 --- a/modules/ensemble/test/cdn_provider_test.dart +++ b/modules/ensemble/test/cdn_provider_test.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/definition_providers/cdn_provider.dart'; import 'package:ensemble/framework/definition_providers/provider.dart'; +import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/util/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; @@ -121,6 +122,21 @@ void main() { final prefs = await SharedPreferences.getInstance(); expect(prefs.getStringList(cacheKey), isNull); }); + + test('rejects manifest without artifacts before mutating in-memory cache', + () async { + final provider = CdnDefinitionProvider('missing-artifacts-app'); + await provider.applyRuntimeManifestForTesting(_manifestWithNewKey()); + + expect(provider.getSupportedLanguages(), isNotEmpty); + + expect( + () => provider.rebuildManifestCacheForTesting({}), + throwsA(isA()), + ); + + expect(provider.getSupportedLanguages(), isNotEmpty); + }); }); group('CDN translation runtime refresh', () {