@hot-updater/react-native 1.0.0-rc.17 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/lib/commonjs/checkForUpdate.js +7 -9
  2. package/lib/commonjs/checkForUpdate.js.map +1 -1
  3. package/lib/commonjs/clientPlugin.js +7 -43
  4. package/lib/commonjs/clientPlugin.js.map +1 -1
  5. package/lib/commonjs/httpClient.js +7 -9
  6. package/lib/commonjs/httpClient.js.map +1 -1
  7. package/lib/commonjs/index.js +10 -0
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/native.js +4 -4
  10. package/lib/commonjs/native.js.map +1 -1
  11. package/lib/commonjs/pluginHost.js +26 -190
  12. package/lib/commonjs/pluginHost.js.map +1 -1
  13. package/lib/commonjs/releaseCatalogCache.js +7 -59
  14. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  15. package/lib/commonjs/sdkVersion.js +1 -1
  16. package/lib/module/checkForUpdate.js +1 -3
  17. package/lib/module/checkForUpdate.js.map +1 -1
  18. package/lib/module/clientPlugin.js +1 -41
  19. package/lib/module/clientPlugin.js.map +1 -1
  20. package/lib/module/httpClient.js +1 -3
  21. package/lib/module/httpClient.js.map +1 -1
  22. package/lib/module/index.js +2 -0
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/native.js +1 -1
  25. package/lib/module/native.js.map +1 -1
  26. package/lib/module/pluginHost.js +22 -187
  27. package/lib/module/pluginHost.js.map +1 -1
  28. package/lib/module/releaseCatalogCache.js +3 -54
  29. package/lib/module/releaseCatalogCache.js.map +1 -1
  30. package/lib/module/sdkVersion.js +1 -1
  31. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/clientPlugin.d.ts +1 -184
  33. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/httpClient.d.ts +1 -2
  35. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/index.d.ts +1 -0
  37. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/native.d.ts +1 -1
  39. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/pluginHost.d.ts +3 -28
  41. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +1 -13
  43. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  45. package/lib/typescript/commonjs/types.d.ts +1 -1
  46. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/wrap.types.d.ts +2 -2
  48. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  49. package/lib/typescript/module/clientPlugin.d.ts +1 -184
  50. package/lib/typescript/module/clientPlugin.d.ts.map +1 -1
  51. package/lib/typescript/module/httpClient.d.ts +1 -2
  52. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  53. package/lib/typescript/module/index.d.ts +1 -0
  54. package/lib/typescript/module/index.d.ts.map +1 -1
  55. package/lib/typescript/module/native.d.ts +1 -1
  56. package/lib/typescript/module/native.d.ts.map +1 -1
  57. package/lib/typescript/module/pluginHost.d.ts +3 -28
  58. package/lib/typescript/module/pluginHost.d.ts.map +1 -1
  59. package/lib/typescript/module/releaseCatalogCache.d.ts +1 -13
  60. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  61. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  62. package/lib/typescript/module/types.d.ts +1 -1
  63. package/lib/typescript/module/types.d.ts.map +1 -1
  64. package/lib/typescript/module/wrap.types.d.ts +2 -2
  65. package/package.json +7 -16
  66. package/src/checkForUpdate.ts +4 -6
  67. package/src/clientPlugin.ts +17 -231
  68. package/src/httpClient.ts +4 -4
  69. package/src/index.ts +7 -0
  70. package/src/native.ts +1 -1
  71. package/src/pluginHost.ts +24 -241
  72. package/src/releaseCatalogCache.ts +7 -136
  73. package/src/sdkVersion.ts +1 -1
  74. package/src/types.ts +1 -1
  75. package/src/wrap.types.ts +2 -2
  76. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  77. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -2236
  78. package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +0 -220
  79. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -264
  80. package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +0 -97
  81. package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +0 -71
  82. package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +0 -106
  83. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  84. package/android/src/test/java/com/hotupdater/NoBackupContext.kt +0 -11
  85. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -397
  86. package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +0 -44
  87. package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +0 -57
  88. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  89. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  90. package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +0 -206
  91. package/lib/commonjs/appReady.spec.js +0 -360
  92. package/lib/commonjs/appReady.spec.js.map +0 -1
  93. package/lib/commonjs/appReady.test-utils.js +0 -25
  94. package/lib/commonjs/appReady.test-utils.js.map +0 -1
  95. package/lib/commonjs/baseURL.js +0 -16
  96. package/lib/commonjs/baseURL.js.map +0 -1
  97. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  98. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  99. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -936
  100. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  101. package/lib/commonjs/entryImportGraph.spec.js +0 -50
  102. package/lib/commonjs/entryImportGraph.spec.js.map +0 -1
  103. package/lib/commonjs/httpClient.spec.js +0 -171
  104. package/lib/commonjs/httpClient.spec.js.map +0 -1
  105. package/lib/commonjs/index.spec.js +0 -395
  106. package/lib/commonjs/index.spec.js.map +0 -1
  107. package/lib/commonjs/iosInterop.spec.js +0 -18
  108. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  109. package/lib/commonjs/native.spec.js +0 -910
  110. package/lib/commonjs/native.spec.js.map +0 -1
  111. package/lib/commonjs/pluginHost.spec.js +0 -292
  112. package/lib/commonjs/pluginHost.spec.js.map +0 -1
  113. package/lib/commonjs/plugins/insights/eventId.js +0 -129
  114. package/lib/commonjs/plugins/insights/eventId.js.map +0 -1
  115. package/lib/commonjs/plugins/insights/eventId.spec.js +0 -47
  116. package/lib/commonjs/plugins/insights/eventId.spec.js.map +0 -1
  117. package/lib/commonjs/plugins/insights/index.js +0 -272
  118. package/lib/commonjs/plugins/insights/index.js.map +0 -1
  119. package/lib/commonjs/plugins/insights/insights.spec.js +0 -640
  120. package/lib/commonjs/plugins/insights/insights.spec.js.map +0 -1
  121. package/lib/commonjs/plugins/insights/sender.js +0 -128
  122. package/lib/commonjs/plugins/insights/sender.js.map +0 -1
  123. package/lib/commonjs/plugins/insights/sender.spec.js +0 -265
  124. package/lib/commonjs/plugins/insights/sender.spec.js.map +0 -1
  125. package/lib/commonjs/plugins/insights/state.js +0 -145
  126. package/lib/commonjs/plugins/insights/state.js.map +0 -1
  127. package/lib/commonjs/releaseCatalogCache.spec.js +0 -213
  128. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  129. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  130. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  131. package/lib/commonjs/store.notification.spec.js +0 -84
  132. package/lib/commonjs/store.notification.spec.js.map +0 -1
  133. package/lib/commonjs/store.spec.js +0 -182
  134. package/lib/commonjs/store.spec.js.map +0 -1
  135. package/lib/commonjs/updateError.spec.js +0 -149
  136. package/lib/commonjs/updateError.spec.js.map +0 -1
  137. package/lib/commonjs/wrap.progress.spec.js +0 -195
  138. package/lib/commonjs/wrap.progress.spec.js.map +0 -1
  139. package/lib/commonjs/wrap.spec.js +0 -232
  140. package/lib/commonjs/wrap.spec.js.map +0 -1
  141. package/lib/module/appReady.spec.js +0 -359
  142. package/lib/module/appReady.spec.js.map +0 -1
  143. package/lib/module/appReady.test-utils.js +0 -19
  144. package/lib/module/appReady.test-utils.js.map +0 -1
  145. package/lib/module/baseURL.js +0 -11
  146. package/lib/module/baseURL.js.map +0 -1
  147. package/lib/module/catalogCacheNative.spec.js +0 -31
  148. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  149. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -935
  150. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  151. package/lib/module/entryImportGraph.spec.js +0 -50
  152. package/lib/module/entryImportGraph.spec.js.map +0 -1
  153. package/lib/module/httpClient.spec.js +0 -171
  154. package/lib/module/httpClient.spec.js.map +0 -1
  155. package/lib/module/index.spec.js +0 -394
  156. package/lib/module/index.spec.js.map +0 -1
  157. package/lib/module/iosInterop.spec.js +0 -18
  158. package/lib/module/iosInterop.spec.js.map +0 -1
  159. package/lib/module/native.spec.js +0 -909
  160. package/lib/module/native.spec.js.map +0 -1
  161. package/lib/module/pluginHost.spec.js +0 -291
  162. package/lib/module/pluginHost.spec.js.map +0 -1
  163. package/lib/module/plugins/insights/eventId.js +0 -122
  164. package/lib/module/plugins/insights/eventId.js.map +0 -1
  165. package/lib/module/plugins/insights/eventId.spec.js +0 -47
  166. package/lib/module/plugins/insights/eventId.spec.js.map +0 -1
  167. package/lib/module/plugins/insights/index.js +0 -268
  168. package/lib/module/plugins/insights/index.js.map +0 -1
  169. package/lib/module/plugins/insights/insights.spec.js +0 -639
  170. package/lib/module/plugins/insights/insights.spec.js.map +0 -1
  171. package/lib/module/plugins/insights/sender.js +0 -123
  172. package/lib/module/plugins/insights/sender.js.map +0 -1
  173. package/lib/module/plugins/insights/sender.spec.js +0 -265
  174. package/lib/module/plugins/insights/sender.spec.js.map +0 -1
  175. package/lib/module/plugins/insights/state.js +0 -139
  176. package/lib/module/plugins/insights/state.js.map +0 -1
  177. package/lib/module/releaseCatalogCache.spec.js +0 -213
  178. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  179. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  180. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  181. package/lib/module/store.notification.spec.js +0 -85
  182. package/lib/module/store.notification.spec.js.map +0 -1
  183. package/lib/module/store.spec.js +0 -181
  184. package/lib/module/store.spec.js.map +0 -1
  185. package/lib/module/updateError.spec.js +0 -149
  186. package/lib/module/updateError.spec.js.map +0 -1
  187. package/lib/module/wrap.progress.spec.js +0 -196
  188. package/lib/module/wrap.progress.spec.js.map +0 -1
  189. package/lib/module/wrap.spec.js +0 -233
  190. package/lib/module/wrap.spec.js.map +0 -1
  191. package/lib/typescript/commonjs/appReady.test-utils.d.ts +0 -9
  192. package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +0 -1
  193. package/lib/typescript/commonjs/baseURL.d.ts +0 -4
  194. package/lib/typescript/commonjs/baseURL.d.ts.map +0 -1
  195. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +0 -18
  196. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +0 -1
  197. package/lib/typescript/commonjs/plugins/insights/index.d.ts +0 -40
  198. package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +0 -1
  199. package/lib/typescript/commonjs/plugins/insights/sender.d.ts +0 -74
  200. package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +0 -1
  201. package/lib/typescript/commonjs/plugins/insights/state.d.ts +0 -46
  202. package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +0 -1
  203. package/lib/typescript/module/appReady.test-utils.d.ts +0 -9
  204. package/lib/typescript/module/appReady.test-utils.d.ts.map +0 -1
  205. package/lib/typescript/module/baseURL.d.ts +0 -4
  206. package/lib/typescript/module/baseURL.d.ts.map +0 -1
  207. package/lib/typescript/module/plugins/insights/eventId.d.ts +0 -18
  208. package/lib/typescript/module/plugins/insights/eventId.d.ts.map +0 -1
  209. package/lib/typescript/module/plugins/insights/index.d.ts +0 -40
  210. package/lib/typescript/module/plugins/insights/index.d.ts.map +0 -1
  211. package/lib/typescript/module/plugins/insights/sender.d.ts +0 -74
  212. package/lib/typescript/module/plugins/insights/sender.d.ts.map +0 -1
  213. package/lib/typescript/module/plugins/insights/state.d.ts +0 -46
  214. package/lib/typescript/module/plugins/insights/state.d.ts.map +0 -1
  215. package/plugins/insights/package.json +0 -8
  216. package/src/appReady.spec.ts +0 -393
  217. package/src/appReady.test-utils.ts +0 -31
  218. package/src/baseURL.ts +0 -15
  219. package/src/catalogCacheNative.spec.ts +0 -63
  220. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -966
  221. package/src/entryImportGraph.spec.ts +0 -65
  222. package/src/httpClient.spec.ts +0 -216
  223. package/src/index.spec.ts +0 -434
  224. package/src/iosInterop.spec.ts +0 -30
  225. package/src/native.spec.ts +0 -978
  226. package/src/pluginHost.spec.ts +0 -297
  227. package/src/plugins/insights/eventId.spec.ts +0 -60
  228. package/src/plugins/insights/eventId.ts +0 -171
  229. package/src/plugins/insights/index.ts +0 -367
  230. package/src/plugins/insights/insights.spec.ts +0 -732
  231. package/src/plugins/insights/sender.spec.ts +0 -287
  232. package/src/plugins/insights/sender.ts +0 -221
  233. package/src/plugins/insights/state.ts +0 -184
  234. package/src/releaseCatalogCache.spec.ts +0 -304
  235. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  236. package/src/store.notification.spec.ts +0 -102
  237. package/src/store.spec.ts +0 -215
  238. package/src/updateError.spec.ts +0 -144
  239. package/src/wrap.progress.spec.ts +0 -232
  240. package/src/wrap.spec.ts +0 -262
@@ -1,978 +0,0 @@
1
- import { INVALID_COHORT_ERROR_MESSAGE } from "@hot-updater/core";
2
- import { beforeEach, describe, expect, it, type Mock, vi } from "vitest";
3
-
4
- const nativeModuleMock = vi.hoisted(() => {
5
- const getManifest = vi.fn<() => Record<string, unknown> | string>();
6
- const getCrashHistory = vi.fn<() => string[] | string>(() => []);
7
-
8
- return {
9
- getActiveUpdateState: vi.fn<() => unknown>(),
10
- clearCrashHistory: vi.fn(() => true),
11
- getBaseURL: vi.fn<() => string | null>(() => null),
12
- getBundleId: vi.fn<() => string | null>(() => "bundle-id"),
13
- getCohort: vi.fn<() => string>(() => "123"),
14
- getInstallId: vi.fn<() => string>(() => "install-id"),
15
- getStorageItem: vi.fn<(key: string) => string | null>(() => null),
16
- getManifest,
17
- getCrashHistory,
18
- getConstants: vi.fn(() => ({
19
- APP_VERSION: null,
20
- CHANNEL: "production",
21
- DEFAULT_CHANNEL: "production",
22
- FINGERPRINT_HASH: null,
23
- MIN_BUNDLE_ID: "min-bundle-id",
24
- })),
25
- notifyAppReady: vi.fn(),
26
- reload: vi.fn(),
27
- resetChannel: vi.fn(),
28
- setCohort: vi.fn(),
29
- setStorageItem: vi.fn<(key: string, value: string | null) => void>(),
30
- setBundleURL: vi.fn(),
31
- switchChannel: vi.fn(),
32
- updateBundle: vi.fn(),
33
- };
34
- });
35
-
36
- vi.mock("react-native", () => ({
37
- NativeEventEmitter: class {
38
- addListener() {
39
- return { remove: () => {} };
40
- }
41
- },
42
- Platform: {
43
- OS: "ios",
44
- },
45
- }));
46
-
47
- vi.mock("./specs/NativeHotUpdater", () => ({
48
- default: nativeModuleMock,
49
- }));
50
-
51
- describe("notifyAppReady", () => {
52
- beforeEach(() => {
53
- vi.resetModules();
54
- nativeModuleMock.getActiveUpdateState.mockReset();
55
- nativeModuleMock.getActiveUpdateState.mockReturnValue({
56
- activeSelection: null,
57
- stableSelection: null,
58
- verificationPending: false,
59
- highestSeenCatalogs: {},
60
- });
61
- nativeModuleMock.notifyAppReady.mockReset();
62
- nativeModuleMock.getBaseURL.mockReset();
63
- nativeModuleMock.getBundleId.mockReset();
64
- nativeModuleMock.getCrashHistory.mockReset();
65
- nativeModuleMock.getConstants.mockReturnValue({
66
- APP_VERSION: null,
67
- CHANNEL: "production",
68
- DEFAULT_CHANNEL: "production",
69
- FINGERPRINT_HASH: null,
70
- MIN_BUNDLE_ID: "min-bundle-id",
71
- });
72
- nativeModuleMock.getBundleId.mockReturnValue("bundle-id");
73
- nativeModuleMock.getBaseURL.mockReturnValue(null);
74
- nativeModuleMock.getCrashHistory.mockReturnValue([]);
75
- nativeModuleMock.getCohort.mockReset();
76
- nativeModuleMock.getCohort.mockReturnValue("123");
77
- nativeModuleMock.getInstallId.mockReset();
78
- nativeModuleMock.getInstallId.mockReturnValue("install-id");
79
- nativeModuleMock.getManifest.mockReset();
80
- nativeModuleMock.getManifest.mockReturnValue({
81
- assets: {
82
- "index.android.bundle": {
83
- fileHash: "hash-123",
84
- },
85
- },
86
- bundleId: "bundle-id",
87
- });
88
- nativeModuleMock.resetChannel.mockReset();
89
- nativeModuleMock.setCohort.mockReset();
90
- nativeModuleMock.getStorageItem.mockReset();
91
- nativeModuleMock.getStorageItem.mockReturnValue(null);
92
- nativeModuleMock.setStorageItem.mockReset();
93
- nativeModuleMock.updateBundle.mockReset();
94
- });
95
-
96
- it("exposes active Release state without internal Catalog identity or history", async () => {
97
- const receipt = {
98
- bundleId: "bundle-123",
99
- releaseId: "release-123",
100
- kind: "BUNDLE",
101
- channel: "production",
102
- catalogId: "private-catalog",
103
- scopeKey: "private-scope",
104
- generation: 10,
105
- catalogHash: "private-hash",
106
- selectionContextHash: "private-context",
107
- };
108
- nativeModuleMock.getActiveUpdateState.mockReturnValue({
109
- activeSelection: receipt,
110
- stableSelection: receipt,
111
- verificationPending: true,
112
- highestSeenCatalogs: {
113
- "private-catalog|private-scope": {
114
- generation: 10,
115
- catalogHash: "private-hash",
116
- },
117
- },
118
- });
119
- const { getPublicActiveUpdateState } = await import("./native");
120
- const selection = {
121
- bundleId: "bundle-123",
122
- releaseId: "release-123",
123
- kind: "BUNDLE",
124
- channel: "production",
125
- };
126
- expect(getPublicActiveUpdateState()).toEqual({
127
- activeSelection: selection,
128
- stableSelection: selection,
129
- verificationPending: true,
130
- });
131
- });
132
-
133
- it("tracks same-file promotions without changing artifact or crash identities", async () => {
134
- const state = {
135
- activeSelection: {
136
- kind: "BUNDLE",
137
- bundleId: "bundle-123",
138
- releaseId: "release-1",
139
- },
140
- };
141
- nativeModuleMock.getActiveUpdateState.mockImplementation(() => state);
142
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
143
- nativeModuleMock.getManifest.mockReturnValue({ assets: {} });
144
- nativeModuleMock.getCrashHistory.mockReturnValue(["bundle-failed"]);
145
- const { getUpdateId, getBundleId, getManifest, getCrashHistory } =
146
- await import("./native");
147
-
148
- expect(getUpdateId()).toBe("release-1");
149
- state.activeSelection.releaseId = "release-2";
150
- expect(getUpdateId()).toBe("release-2");
151
- expect(getBundleId()).toBe("bundle-123");
152
- expect(getManifest().bundleId).toBe("bundle-123");
153
- expect(getCrashHistory()).toEqual(["bundle-failed"]);
154
- });
155
-
156
- it.each([
157
- [null, "min-bundle-id"],
158
- ["legacy-bundle", "legacy-bundle"],
159
- ])(
160
- "preserves the fallback ID for native bundle %s",
161
- async (bundleId, expected) => {
162
- nativeModuleMock.getBundleId.mockReturnValue(bundleId);
163
- const { getUpdateId } = await import("./native");
164
-
165
- expect(getUpdateId()).toBe(expected);
166
- },
167
- );
168
-
169
- it("reports a staged update while the manifest still identifies the running file", async () => {
170
- const runningSelection = {
171
- kind: "BUNDLE",
172
- bundleId: "running-file",
173
- releaseId: "running-update",
174
- };
175
- const state = {
176
- activeSelection: runningSelection,
177
- stableSelection: runningSelection,
178
- };
179
- nativeModuleMock.getActiveUpdateState.mockImplementation(() => state);
180
- nativeModuleMock.getBundleId.mockReturnValue("running-file");
181
- nativeModuleMock.getManifest.mockReturnValue({
182
- assets: {},
183
- bundleId: "running-file",
184
- });
185
- const { getUpdateId, getBundleId, getManifest } = await import("./native");
186
-
187
- expect(getUpdateId()).toBe("running-update");
188
- state.activeSelection = {
189
- kind: "BUNDLE",
190
- bundleId: "staged-file",
191
- releaseId: "staged-update",
192
- };
193
-
194
- expect(getUpdateId()).toBe("staged-update");
195
- expect(getBundleId()).toBe("running-file");
196
- expect(getManifest().bundleId).toBe("running-file");
197
- });
198
-
199
- it("reports an explicit built-in rollback by its update ID", async () => {
200
- nativeModuleMock.getActiveUpdateState.mockReturnValue(
201
- JSON.stringify({
202
- activeSelection: {
203
- kind: "EMBEDDED",
204
- releaseId: "rollback-1",
205
- bundleId: "min-bundle-id",
206
- },
207
- }),
208
- );
209
- nativeModuleMock.getBundleId.mockReturnValue(null);
210
- const { getUpdateId, getBundleId } = await import("./native");
211
-
212
- expect(getUpdateId()).toBe("rollback-1");
213
- expect(getBundleId()).toBe("min-bundle-id");
214
- });
215
-
216
- it("normalizes legacy PROMOTED launch reports to UPDATE_APPLIED", async () => {
217
- nativeModuleMock.notifyAppReady.mockReturnValue(
218
- JSON.stringify({
219
- fromBundleId: "bundle-123",
220
- status: "PROMOTED",
221
- toBundleId: "bundle-456",
222
- }),
223
- );
224
-
225
- const { notifyAppReady } = await import("./native");
226
-
227
- expect(notifyAppReady()).toEqual({
228
- fromBundleId: "bundle-123",
229
- status: "UPDATE_APPLIED",
230
- toBundleId: "bundle-456",
231
- });
232
- expect(nativeModuleMock.notifyAppReady).toHaveBeenCalledWith();
233
- });
234
-
235
- it("returns RECOVERED launch reports with directional ids", async () => {
236
- nativeModuleMock.notifyAppReady.mockReturnValue({
237
- fromBundleId: "bundle-123",
238
- status: "RECOVERED",
239
- toBundleId: "bundle-122",
240
- updateStrategy: "appVersion",
241
- });
242
-
243
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
244
-
245
- expect(notifyAppReady()).toEqual({
246
- fromBundleId: "bundle-123",
247
- status: "RECOVERED",
248
- toBundleId: "bundle-122",
249
- });
250
- expect(readNotifyAppReady()).toEqual({
251
- previousProcessExit: null,
252
- transition: {
253
- fromBundleId: "bundle-123",
254
- fromReleaseId: null,
255
- toBundleId: "bundle-122",
256
- toReleaseId: null,
257
- type: "RECOVERED",
258
- updateStrategy: "appVersion",
259
- },
260
- pending: false,
261
- result: {
262
- fromBundleId: "bundle-123",
263
- status: "RECOVERED",
264
- toBundleId: "bundle-122",
265
- },
266
- });
267
- });
268
-
269
- it("returns UNCHANGED for incomplete recovery payloads", async () => {
270
- nativeModuleMock.notifyAppReady.mockReturnValue({
271
- fromBundleId: "bundle-123",
272
- status: "RECOVERED",
273
- toBundleId: null,
274
- updateStrategy: "appVersion",
275
- });
276
-
277
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
278
-
279
- expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
280
- expect(readNotifyAppReady()).toEqual({
281
- previousProcessExit: null,
282
- transition: null,
283
- pending: false,
284
- result: { status: "UNCHANGED" },
285
- });
286
- });
287
-
288
- it("returns UNCHANGED when an applied launch report has no bundle ids", async () => {
289
- nativeModuleMock.notifyAppReady.mockReturnValue({
290
- status: "UPDATE_APPLIED",
291
- });
292
-
293
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
294
-
295
- expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
296
- expect(readNotifyAppReady()).toEqual({
297
- previousProcessExit: null,
298
- transition: null,
299
- pending: false,
300
- result: { status: "UNCHANGED" },
301
- });
302
- });
303
-
304
- it("normalizes malformed old-arch notifyAppReady payloads to UNCHANGED", async () => {
305
- nativeModuleMock.notifyAppReady.mockReturnValue("{");
306
-
307
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
308
-
309
- expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
310
- expect(readNotifyAppReady()).toEqual({
311
- previousProcessExit: null,
312
- transition: null,
313
- pending: false,
314
- result: { status: "UNCHANGED" },
315
- });
316
- });
317
-
318
- it("keeps the internal pending state out of the public result", async () => {
319
- nativeModuleMock.notifyAppReady.mockReturnValue({ status: "PENDING" });
320
-
321
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
322
-
323
- expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
324
- expect(readNotifyAppReady()).toEqual({
325
- previousProcessExit: null,
326
- transition: null,
327
- pending: true,
328
- result: { status: "UNCHANGED" },
329
- });
330
- });
331
-
332
- it("returns the native bundle id when available", async () => {
333
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
334
-
335
- const { getBundleId } = await import("./native");
336
-
337
- expect(getBundleId()).toBe("bundle-123");
338
- });
339
-
340
- it("throws when native SDK does not expose getBundleId", async () => {
341
- const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
342
- getBundleId?: typeof nativeModuleMock.getBundleId;
343
- };
344
- const originalGetBundleId = nativeModule.getBundleId;
345
- nativeModule.getBundleId = null as unknown as Mock<() => string | null>;
346
-
347
- try {
348
- const { getBundleId } = await import("./native");
349
-
350
- expect(() => getBundleId()).toThrow(
351
- "Native module is missing 'getBundleId()'",
352
- );
353
- } finally {
354
- nativeModule.getBundleId = originalGetBundleId;
355
- }
356
- });
357
-
358
- it("falls back to MIN_BUNDLE_ID when native reports an empty bundle id", async () => {
359
- nativeModuleMock.getBundleId.mockReturnValue("");
360
-
361
- const { getBundleId } = await import("./native");
362
-
363
- expect(getBundleId()).toBe("min-bundle-id");
364
- });
365
-
366
- it("falls back to MIN_BUNDLE_ID when native bundle id is null", async () => {
367
- nativeModuleMock.getBundleId.mockReturnValue(null);
368
-
369
- const { getBundleId } = await import("./native");
370
-
371
- expect(getBundleId()).toBe("min-bundle-id");
372
- });
373
-
374
- it("falls back to MIN_BUNDLE_ID for legacy NIL_UUID bundle ids", async () => {
375
- nativeModuleMock.getBundleId.mockReturnValue(
376
- "00000000-0000-0000-0000-000000000000",
377
- );
378
-
379
- const { getBundleId } = await import("./native");
380
-
381
- expect(getBundleId()).toBe("min-bundle-id");
382
- });
383
-
384
- it("returns manifest from native objects", async () => {
385
- nativeModuleMock.getManifest.mockReturnValue({
386
- assets: {
387
- "assets/logo.png": {
388
- fileHash: "hash-logo",
389
- signature: "sig-logo",
390
- },
391
- "index.android.bundle": {
392
- fileHash: "hash-bundle",
393
- },
394
- },
395
- bundleId: "bundle-123",
396
- });
397
-
398
- const { getManifest } = await import("./native");
399
-
400
- expect(getManifest()).toEqual({
401
- assets: {
402
- "assets/logo.png": {
403
- fileHash: "hash-logo",
404
- signature: "sig-logo",
405
- },
406
- "index.android.bundle": {
407
- fileHash: "hash-bundle",
408
- },
409
- },
410
- bundleId: "bundle-123",
411
- });
412
- });
413
-
414
- it("normalizes legacy manifest asset entries from native objects", async () => {
415
- nativeModuleMock.getManifest.mockReturnValue({
416
- assets: {
417
- "assets/logo.png": "hash-logo",
418
- },
419
- bundleId: "bundle-123",
420
- });
421
-
422
- const { getManifest } = await import("./native");
423
-
424
- expect(getManifest()).toEqual({
425
- assets: {
426
- "assets/logo.png": {
427
- fileHash: "hash-logo",
428
- },
429
- },
430
- bundleId: "bundle-123",
431
- });
432
- });
433
-
434
- it("parses manifest from old-arch JSON payloads", async () => {
435
- nativeModuleMock.getManifest.mockReturnValue(
436
- JSON.stringify({
437
- assets: {
438
- "assets/logo.png": {
439
- fileHash: "hash-logo",
440
- },
441
- },
442
- bundleId: "bundle-123",
443
- }),
444
- );
445
-
446
- const { getManifest } = await import("./native");
447
-
448
- expect(getManifest()).toEqual({
449
- assets: {
450
- "assets/logo.png": {
451
- fileHash: "hash-logo",
452
- },
453
- },
454
- bundleId: "bundle-123",
455
- });
456
- });
457
-
458
- it("normalizes legacy manifest asset entries from old-arch JSON payloads", async () => {
459
- nativeModuleMock.getManifest.mockReturnValue(
460
- JSON.stringify({
461
- assets: {
462
- "assets/logo.png": "hash-logo",
463
- },
464
- bundleId: "bundle-123",
465
- }),
466
- );
467
-
468
- const { getManifest } = await import("./native");
469
-
470
- expect(getManifest()).toEqual({
471
- assets: {
472
- "assets/logo.png": {
473
- fileHash: "hash-logo",
474
- },
475
- },
476
- bundleId: "bundle-123",
477
- });
478
- });
479
-
480
- it("returns an empty-assets manifest for malformed payloads", async () => {
481
- nativeModuleMock.getManifest.mockReturnValue("{");
482
-
483
- const { getManifest } = await import("./native");
484
-
485
- expect(getManifest()).toEqual({
486
- assets: {},
487
- bundleId: "bundle-id",
488
- });
489
- });
490
-
491
- it("caches active bundle getters within a JS runtime", async () => {
492
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
493
- nativeModuleMock.getManifest.mockReturnValue({
494
- assets: {
495
- "assets/logo.png": {
496
- fileHash: "hash-logo",
497
- },
498
- },
499
- bundleId: "bundle-123",
500
- });
501
- nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-123");
502
-
503
- const { getBaseURL, getBundleId, getManifest } = await import("./native");
504
-
505
- expect(getBundleId()).toBe("bundle-123");
506
- expect(getBundleId()).toBe("bundle-123");
507
- expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(1);
508
-
509
- const firstManifest = getManifest();
510
- firstManifest.assets["assets/logo.png"] = {
511
- fileHash: "mutated-hash",
512
- };
513
-
514
- expect(getManifest()).toEqual({
515
- assets: {
516
- "assets/logo.png": {
517
- fileHash: "hash-logo",
518
- },
519
- },
520
- bundleId: "bundle-123",
521
- });
522
- expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(1);
523
-
524
- expect(getBaseURL()).toBe("file:///bundle-123");
525
- expect(getBaseURL()).toBe("file:///bundle-123");
526
- expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(1);
527
- });
528
-
529
- it("uses the launched bundle reported by native after updateBundle succeeds", async () => {
530
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
531
- nativeModuleMock.getManifest.mockReturnValue({
532
- assets: {},
533
- bundleId: "bundle-123",
534
- });
535
- nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-123");
536
- nativeModuleMock.updateBundle.mockResolvedValue(true);
537
-
538
- const { getBaseURL, getBundleId, getManifest, updateBundle } =
539
- await import("./native");
540
-
541
- expect(getBundleId()).toBe("bundle-123");
542
- expect(getManifest()).toEqual({
543
- assets: {},
544
- bundleId: "bundle-123",
545
- });
546
- expect(getBaseURL()).toBe("file:///bundle-123");
547
-
548
- await updateBundle({
549
- assets: {},
550
- bundleId: "bundle-456",
551
- archiveUrl: "https://example.com/bundle.tar.br",
552
- manifestFileHash: "manifest-hash",
553
- manifestUrl: "https://example.com/manifest.json",
554
- status: "UPDATE",
555
- });
556
-
557
- expect(getBundleId()).toBe("bundle-123");
558
- expect(nativeModuleMock.updateBundle).toHaveBeenCalledWith(
559
- expect.objectContaining({
560
- archiveUrl: "https://example.com/bundle.tar.br",
561
- }),
562
- );
563
- expect(getManifest()).toEqual({
564
- assets: {},
565
- bundleId: "bundle-123",
566
- });
567
- expect(getBaseURL()).toBe("file:///bundle-123");
568
- expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(3);
569
- expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(2);
570
- expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(2);
571
- });
572
-
573
- it("reinstalls a session-installed bundle after native resets to built-in", async () => {
574
- nativeModuleMock.getConstants.mockReturnValue({
575
- APP_VERSION: null,
576
- CHANNEL: "production",
577
- DEFAULT_CHANNEL: "production",
578
- FINGERPRINT_HASH: null,
579
- MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
580
- });
581
- nativeModuleMock.getBundleId.mockReturnValue(
582
- "00000000-0000-0000-0000-000000000002",
583
- );
584
- nativeModuleMock.updateBundle.mockResolvedValue(true);
585
-
586
- const { getBundleId, updateBundle } = await import("./native");
587
-
588
- await updateBundle({
589
- assets: {},
590
- bundleId: "00000000-0000-0000-0000-000000000003",
591
- manifestFileHash: "manifest-hash",
592
- manifestUrl: "https://example.com/manifest.json",
593
- status: "UPDATE",
594
- });
595
-
596
- nativeModuleMock.getBundleId.mockReturnValue(
597
- "00000000-0000-0000-0000-000000000003",
598
- );
599
- expect(getBundleId()).toBe("00000000-0000-0000-0000-000000000003");
600
-
601
- nativeModuleMock.getBundleId.mockReturnValue(null);
602
-
603
- await expect(
604
- updateBundle({
605
- assets: {},
606
- bundleId: "00000000-0000-0000-0000-000000000003",
607
- manifestFileHash: "manifest-hash",
608
- manifestUrl: "https://example.com/manifest.json",
609
- status: "UPDATE",
610
- }),
611
- ).resolves.toBe(true);
612
-
613
- expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(2);
614
- });
615
-
616
- it("skips a session-installed bundle when native still reports it active", async () => {
617
- nativeModuleMock.getConstants.mockReturnValue({
618
- APP_VERSION: null,
619
- CHANNEL: "production",
620
- DEFAULT_CHANNEL: "production",
621
- FINGERPRINT_HASH: null,
622
- MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
623
- });
624
- nativeModuleMock.getBundleId.mockReturnValue(
625
- "00000000-0000-0000-0000-000000000002",
626
- );
627
- nativeModuleMock.updateBundle.mockResolvedValue(true);
628
-
629
- const { updateBundle } = await import("./native");
630
-
631
- await updateBundle({
632
- assets: {},
633
- bundleId: "00000000-0000-0000-0000-000000000003",
634
- manifestFileHash: "manifest-hash",
635
- manifestUrl: "https://example.com/manifest.json",
636
- status: "UPDATE",
637
- });
638
-
639
- nativeModuleMock.getBundleId.mockReturnValue(
640
- "00000000-0000-0000-0000-000000000003",
641
- );
642
-
643
- await expect(
644
- updateBundle({
645
- assets: {},
646
- bundleId: "00000000-0000-0000-0000-000000000003",
647
- manifestFileHash: "manifest-hash",
648
- manifestUrl: "https://example.com/manifest.json",
649
- status: "UPDATE",
650
- }),
651
- ).resolves.toBe(true);
652
-
653
- expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(1);
654
- });
655
-
656
- it("forwards manifest artifact parameters to native updateBundle", async () => {
657
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
658
- nativeModuleMock.updateBundle.mockResolvedValue(true);
659
-
660
- const { updateBundle } = await import("./native");
661
-
662
- await updateBundle({
663
- bundleId: "bundle-789",
664
- assets: {
665
- "index.ios.bundle": {
666
- file: {
667
- compression: "br",
668
- url: "https://example.com/files/index.ios.bundle.br",
669
- },
670
- fileHash: "hash-next",
671
- patch: {
672
- algorithm: "bsdiff",
673
- baseBundleId: "bundle-123",
674
- baseFileHash: "hash-prev",
675
- patchFileHash: "hash-patch",
676
- patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
677
- },
678
- },
679
- },
680
- manifestFileHash: "sig:manifest",
681
- manifestUrl: "https://example.com/manifest.json",
682
- status: "UPDATE",
683
- });
684
-
685
- expect(nativeModuleMock.updateBundle).toHaveBeenCalledWith({
686
- bundleId: "bundle-789",
687
- assets: {
688
- "index.ios.bundle": {
689
- file: {
690
- compression: "br",
691
- url: "https://example.com/files/index.ios.bundle.br",
692
- },
693
- fileHash: "hash-next",
694
- patch: {
695
- algorithm: "bsdiff",
696
- baseBundleId: "bundle-123",
697
- baseFileHash: "hash-prev",
698
- patchFileHash: "hash-patch",
699
- patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
700
- },
701
- },
702
- },
703
- channel: undefined,
704
- manifestFileHash: "sig:manifest",
705
- manifestUrl: "https://example.com/manifest.json",
706
- });
707
- });
708
-
709
- it("resolves how native delivered a staged bundle", async () => {
710
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
711
- nativeModuleMock.updateBundle.mockResolvedValue({
712
- delivery: "patch",
713
- patchFallback: true,
714
- });
715
- const { stageBundle, updateBundle } = await import("./native");
716
- const params = {
717
- assets: {},
718
- bundleId: "bundle-789",
719
- manifestFileHash: "sig:manifest",
720
- manifestUrl: "https://example.com/manifest.json",
721
- status: "UPDATE" as const,
722
- };
723
-
724
- await expect(stageBundle(params)).resolves.toEqual({
725
- delivery: "patch",
726
- patchFallback: true,
727
- });
728
- // Native still reports bundle-123, so the same bundle downloads again.
729
- await expect(updateBundle(params)).resolves.toBe(true);
730
- nativeModuleMock.getBundleId.mockReturnValue("bundle-789");
731
- // Now native reports it: this runtime already staged it.
732
- await expect(stageBundle(params)).resolves.toBeNull();
733
- expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(2);
734
- });
735
-
736
- it("passes on why the previous process exited from the launch report", async () => {
737
- nativeModuleMock.notifyAppReady.mockReturnValue({
738
- previousProcessExit: "LOW_MEMORY",
739
- status: "UNCHANGED",
740
- });
741
-
742
- const { readNotifyAppReady } = await import("./native");
743
-
744
- expect(readNotifyAppReady()).toEqual({
745
- previousProcessExit: "LOW_MEMORY",
746
- transition: null,
747
- pending: false,
748
- result: { status: "UNCHANGED" },
749
- });
750
- });
751
-
752
- it("invalidates cached bundle getters after resetChannel succeeds", async () => {
753
- nativeModuleMock.getConstants.mockReturnValue({
754
- APP_VERSION: null,
755
- CHANNEL: "beta",
756
- DEFAULT_CHANNEL: "production",
757
- FINGERPRINT_HASH: null,
758
- MIN_BUNDLE_ID: "min-bundle-id",
759
- });
760
- nativeModuleMock.getBundleId.mockReturnValue("bundle-beta");
761
- nativeModuleMock.getManifest.mockReturnValue({
762
- assets: {},
763
- bundleId: "bundle-beta",
764
- });
765
- nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-beta");
766
- nativeModuleMock.resetChannel.mockResolvedValue(true);
767
-
768
- const { getBaseURL, getBundleId, getManifest, resetChannel } =
769
- await import("./native");
770
-
771
- expect(getBundleId()).toBe("bundle-beta");
772
- expect(getManifest()).toEqual({
773
- assets: {},
774
- bundleId: "bundle-beta",
775
- });
776
- expect(getBaseURL()).toBe("file:///bundle-beta");
777
-
778
- nativeModuleMock.getBundleId.mockReturnValue(null);
779
- nativeModuleMock.getManifest.mockReturnValue({});
780
- nativeModuleMock.getBaseURL.mockReturnValue("");
781
-
782
- await expect(resetChannel()).resolves.toBe(true);
783
-
784
- expect(getBundleId()).toBe("min-bundle-id");
785
- expect(getManifest()).toEqual({
786
- assets: {},
787
- bundleId: "min-bundle-id",
788
- });
789
- expect(getBaseURL()).toBeNull();
790
- expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(2);
791
- expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(2);
792
- expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(2);
793
- });
794
-
795
- it("delegates resetChannel to native even when exported channel constants already match", async () => {
796
- nativeModuleMock.getConstants.mockReturnValue({
797
- APP_VERSION: null,
798
- CHANNEL: "beta",
799
- DEFAULT_CHANNEL: "beta",
800
- FINGERPRINT_HASH: null,
801
- MIN_BUNDLE_ID: "min-bundle-id",
802
- });
803
- nativeModuleMock.resetChannel.mockResolvedValue(true);
804
-
805
- const { resetChannel } = await import("./native");
806
-
807
- await expect(resetChannel()).resolves.toBe(true);
808
- expect(nativeModuleMock.resetChannel).toHaveBeenCalledTimes(1);
809
- });
810
-
811
- it("parses crash history from legacy JSON payloads", async () => {
812
- nativeModuleMock.getCrashHistory.mockReturnValue(
813
- JSON.stringify(["bundle-1", "bundle-2"]),
814
- );
815
-
816
- const { getCrashHistory } = await import("./native");
817
-
818
- expect(getCrashHistory()).toEqual(["bundle-1", "bundle-2"]);
819
- });
820
-
821
- it("falls back to an empty crash history for malformed payloads", async () => {
822
- nativeModuleMock.getCrashHistory.mockReturnValue("{");
823
-
824
- const { getCrashHistory } = await import("./native");
825
-
826
- expect(getCrashHistory()).toEqual([]);
827
- });
828
-
829
- it("passes normalized cohort overrides to native", async () => {
830
- const { setCohort } = await import("./native");
831
-
832
- setCohort(" QA-Group ");
833
-
834
- expect(nativeModuleMock.setCohort).toHaveBeenCalledWith("qa-group");
835
- });
836
-
837
- it("returns the most recently set cohort before native reads catch up", async () => {
838
- nativeModuleMock.getCohort.mockReturnValue("123");
839
-
840
- const { getCohort, setCohort } = await import("./native");
841
-
842
- setCohort(" QA-Group ");
843
-
844
- expect(getCohort()).toBe("qa-group");
845
- expect(nativeModuleMock.getCohort).not.toHaveBeenCalled();
846
- });
847
-
848
- it("throws when attempting to clear the cohort with an empty value", async () => {
849
- const { setCohort } = await import("./native");
850
-
851
- expect(() => setCohort("")).toThrow(INVALID_COHORT_ERROR_MESSAGE);
852
- expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
853
- });
854
-
855
- it("throws for invalid cohort overrides", async () => {
856
- const { setCohort } = await import("./native");
857
-
858
- expect(() => setCohort("Bad Cohort")).toThrow(INVALID_COHORT_ERROR_MESSAGE);
859
- expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
860
- });
861
-
862
- it("throws for cohort overrides longer than the limit", async () => {
863
- const { setCohort } = await import("./native");
864
-
865
- expect(() => setCohort("a".repeat(65))).toThrow(
866
- INVALID_COHORT_ERROR_MESSAGE,
867
- );
868
- expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
869
- });
870
-
871
- it("returns the install id reported by native", async () => {
872
- nativeModuleMock.getInstallId.mockReturnValue("install-123");
873
-
874
- const { getInstallId } = await import("./native");
875
-
876
- expect(getInstallId()).toBe("install-123");
877
- });
878
-
879
- it("normalizes an omitted native fingerprint constant to null", async () => {
880
- nativeModuleMock.getConstants.mockReturnValue({
881
- APP_VERSION: null,
882
- CHANNEL: "production",
883
- DEFAULT_CHANNEL: "production",
884
- FINGERPRINT_HASH: undefined as unknown as null,
885
- MIN_BUNDLE_ID: "min-bundle-id",
886
- });
887
-
888
- const { getFingerprintHash } = await import("./native");
889
-
890
- expect(getFingerprintHash()).toBeNull();
891
- });
892
-
893
- it("throws when native SDK does not expose getInstallId", async () => {
894
- const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
895
- getInstallId?: typeof nativeModuleMock.getInstallId;
896
- };
897
- const originalGetInstallId = nativeModule.getInstallId;
898
- nativeModule.getInstallId = null as unknown as Mock<() => string>;
899
-
900
- try {
901
- const { getInstallId } = await import("./native");
902
-
903
- expect(() => getInstallId()).toThrow(
904
- "Native module is missing 'getInstallId()'",
905
- );
906
- } finally {
907
- nativeModule.getInstallId = originalGetInstallId;
908
- }
909
- });
910
-
911
- it("reads and writes the native key-value store", async () => {
912
- nativeModuleMock.getStorageItem.mockImplementation((key) =>
913
- key === "plugins/example/state" ? "stored" : null,
914
- );
915
- const { getStorageItem, setStorageItem } = await import("./native");
916
-
917
- expect(getStorageItem("plugins/example/state")).toBe("stored");
918
- expect(getStorageItem("plugins/example/missing")).toBeNull();
919
- setStorageItem("plugins/example/state", "next");
920
- setStorageItem("plugins/example/state", null);
921
-
922
- expect(nativeModuleMock.setStorageItem).toHaveBeenNthCalledWith(
923
- 1,
924
- "plugins/example/state",
925
- "next",
926
- );
927
- expect(nativeModuleMock.setStorageItem).toHaveBeenNthCalledWith(
928
- 2,
929
- "plugins/example/state",
930
- null,
931
- );
932
- });
933
-
934
- it.each(["getStorageItem", "setStorageItem"] as const)(
935
- "throws when native SDK does not expose %s",
936
- async (name) => {
937
- const nativeModule = nativeModuleMock as Record<string, unknown>;
938
- const original = nativeModule[name];
939
- nativeModule[name] = undefined;
940
-
941
- try {
942
- const { getStorageItem, setStorageItem } = await import("./native");
943
-
944
- expect(() =>
945
- name === "getStorageItem"
946
- ? getStorageItem("key")
947
- : setStorageItem("key", "value"),
948
- ).toThrow(`Native module is missing '${name}()'`);
949
- } finally {
950
- nativeModule[name] = original;
951
- }
952
- },
953
- );
954
-
955
- it("returns the cohort reported by native", async () => {
956
- nativeModuleMock.getCohort.mockReturnValue("qa-group");
957
-
958
- const { getCohort } = await import("./native");
959
-
960
- expect(getCohort()).toBe("qa-group");
961
- });
962
-
963
- it("normalizes the cohort reported by native", async () => {
964
- nativeModuleMock.getCohort.mockReturnValue(" QA-GROUP ");
965
-
966
- const { getCohort } = await import("./native");
967
-
968
- expect(getCohort()).toBe("qa-group");
969
- });
970
-
971
- it("throws when native reports an invalid cohort", async () => {
972
- nativeModuleMock.getCohort.mockReturnValue("1001");
973
-
974
- const { getCohort } = await import("./native");
975
-
976
- expect(() => getCohort()).toThrow(INVALID_COHORT_ERROR_MESSAGE);
977
- });
978
- });