@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,966 +0,0 @@
1
- import {
2
- createReleaseCatalogScopeKey,
3
- encodeChannelKey,
4
- type ArtifactInfo,
5
- type PersistedSelectionReceipt,
6
- type ReleaseCatalog,
7
- } from "@hot-updater/core";
8
- import { beforeEach, describe, expect, it, vi } from "vitest";
9
-
10
- import type { HotUpdaterClientHooks } from "./clientPlugin";
11
- import type { HotUpdaterHttpClient } from "./httpClient";
12
- import { InvalidUpdateResponseError, UpdateHttpError } from "./updateError";
13
-
14
- const MINIMUM_RELEASE_ID = "00000000-0000-7000-8000-000000000001";
15
- const RELEASE_ID = "00000000-0000-7000-8000-000000000002";
16
- const ACTIVE_RELEASE_ID = "00000000-0000-7000-8000-000000000003";
17
- const CURRENT_BUNDLE_ID = "00000000-0000-7001-8000-000000000001";
18
- const TARGET_BUNDLE_ID = "00000000-0000-7001-8000-000000000002";
19
- const CATALOG_ID = "project-a";
20
- const CHANNEL = "production";
21
- const CATALOG_HASH = `sha256:${"a".repeat(64)}`;
22
- const SCOPE_KEY = createReleaseCatalogScopeKey({
23
- channelKey: encodeChannelKey(CHANNEL),
24
- platform: "ios",
25
- strategy: "APP_VERSION",
26
- });
27
-
28
- const mocks = vi.hoisted(() => {
29
- Reflect.set(globalThis, "HotUpdater", { SDK_VERSION: "test-sdk-version" });
30
- return {
31
- addListener: vi.fn(() => () => {}),
32
- acceptReleaseCatalog: vi.fn(() => true),
33
- commitReleaseSelection: vi.fn(async () => true),
34
- getActiveUpdateState: vi.fn(() => ({
35
- activeSelection: null as PersistedSelectionReceipt | null,
36
- highestSeenCatalogs: {},
37
- stableSelection: null as PersistedSelectionReceipt | null,
38
- verificationPending: false,
39
- })),
40
- getAppVersion: vi.fn(() => "1.2"),
41
- getBundleId: vi.fn(() => CURRENT_BUNDLE_ID),
42
- getChannel: vi.fn(() => CHANNEL),
43
- getCohort: vi.fn(() => "123"),
44
- getCrashHistory: vi.fn(() => []),
45
- getDefaultChannel: vi.fn(() => CHANNEL),
46
- getFingerprintHash: vi.fn(() => null),
47
- getInstallId: vi.fn(() => "install-id"),
48
- getMinBundleId: vi.fn(() => MINIMUM_RELEASE_ID),
49
- isChannelSwitched: vi.fn(() => false),
50
- isReleaseSelectionCurrent: vi.fn(() => true),
51
- resetChannel: vi.fn(),
52
- stageBundle: vi.fn(
53
- async (): Promise<{
54
- delivery: "patch" | "manifest" | "archive";
55
- patchFallback: boolean;
56
- } | null> => ({ delivery: "manifest", patchFallback: false }),
57
- ),
58
- };
59
- });
60
-
61
- vi.mock("react-native", () => ({ Platform: { OS: "ios" } }));
62
-
63
- vi.mock("./native", () => mocks);
64
-
65
- const createCatalog = (
66
- overrides: Partial<ReleaseCatalog> = {},
67
- ): ReleaseCatalog => ({
68
- catalogId: CATALOG_ID,
69
- catalogHash: CATALOG_HASH,
70
- fallbackPolicy: "BUILTIN_IF_ACTIVE_INELIGIBLE",
71
- generation: 2,
72
- releases: [
73
- {
74
- bundleId: TARGET_BUNDLE_ID,
75
- kind: "BUNDLE",
76
- message: "Release two",
77
- releaseId: RELEASE_ID,
78
- rolloutCohortCount: 1000,
79
- shouldForceUpdate: false,
80
- targetCohorts: [],
81
- },
82
- ],
83
- schemaVersion: 1,
84
- scopeKey: SCOPE_KEY,
85
- ...overrides,
86
- });
87
-
88
- const createClient = (catalog = createCatalog()) => {
89
- const fetchReleaseCatalog = vi.fn(async () => catalog);
90
- const artifact: ArtifactInfo = {
91
- artifactProtocolVersion: 1,
92
- assets: {},
93
- manifestFileHash: "manifest-hash",
94
- manifestUrl: "https://updates.example.com/manifest.json",
95
- archiveUrl: "https://updates.example.com/bundle.tar.br",
96
- };
97
- const resolveArtifact = vi.fn(async () => artifact);
98
- const session = {
99
- fetchReleaseCatalog,
100
- resolveArtifact,
101
- };
102
- const client: HotUpdaterHttpClient = {
103
- createSession: vi.fn(async () => session),
104
- };
105
- return { client, fetchReleaseCatalog, resolveArtifact };
106
- };
107
-
108
- type PluginEvent = {
109
- [K in keyof HotUpdaterClientHooks]-?: [
110
- K,
111
- Parameters<NonNullable<HotUpdaterClientHooks[K]>>[0],
112
- ];
113
- }[keyof HotUpdaterClientHooks];
114
-
115
- /** Records what checks report to plugins, in order. */
116
- const recordPluginEvents = async () => {
117
- const { configurePlugins } = await import("./pluginHost");
118
- const events: PluginEvent[] = [];
119
- configurePlugins(
120
- [
121
- {
122
- id: "recorder",
123
- setup: () => ({
124
- onUpdateCheck: (result) => {
125
- events.push(["onUpdateCheck", result]);
126
- },
127
- onBundleDownloaded: (info) => {
128
- events.push(["onBundleDownloaded", info]);
129
- },
130
- onUpdateError: (error) => {
131
- events.push(["onUpdateError", error]);
132
- },
133
- }),
134
- },
135
- ],
136
- { baseURL: "https://updates.example.com" },
137
- );
138
- return events;
139
- };
140
-
141
- describe("checkForUpdate Release catalog protocol", () => {
142
- beforeEach(() => {
143
- vi.resetModules();
144
- vi.clearAllMocks();
145
- vi.stubGlobal("__DEV__", false);
146
- mocks.acceptReleaseCatalog.mockReturnValue(true);
147
- mocks.getActiveUpdateState.mockReturnValue({
148
- activeSelection: null,
149
- highestSeenCatalogs: {},
150
- stableSelection: null,
151
- verificationPending: false,
152
- });
153
- mocks.isReleaseSelectionCurrent.mockReturnValue(true);
154
- mocks.commitReleaseSelection.mockResolvedValue(true);
155
- mocks.stageBundle.mockResolvedValue({
156
- delivery: "manifest",
157
- patchFallback: false,
158
- });
159
- });
160
-
161
- it("reports the check, then the download only after staging", async () => {
162
- const { checkForUpdate } = await import("./checkForUpdate");
163
- const events = await recordPluginEvents();
164
- const { client } = createClient();
165
- let complete!: (delivery: {
166
- delivery: "patch" | "manifest" | "archive";
167
- patchFallback: boolean;
168
- }) => void;
169
- mocks.stageBundle.mockImplementationOnce(
170
- () =>
171
- new Promise((resolve) => {
172
- complete = resolve;
173
- }),
174
- );
175
- const update = await checkForUpdate({
176
- client,
177
- updateStrategy: "appVersion",
178
- });
179
- expect(update?.shouldForceUpdate).toBe(false);
180
- expect(events).toEqual([
181
- [
182
- "onUpdateCheck",
183
- {
184
- status: "UPDATE_AVAILABLE",
185
- channel: CHANNEL,
186
- fromBundleId: CURRENT_BUNDLE_ID,
187
- fromReleaseId: null,
188
- toBundleId: TARGET_BUNDLE_ID,
189
- toReleaseId: RELEASE_ID,
190
- transitionKind: "INSTALL",
191
- updateStatus: "UPDATE",
192
- shouldForceUpdate: false,
193
- updateStrategy: "appVersion",
194
- },
195
- ],
196
- ]);
197
- const pending = update!.updateBundle();
198
- await vi.waitFor(() => expect(mocks.stageBundle).toHaveBeenCalledOnce());
199
- expect(events).toHaveLength(1);
200
- complete({ delivery: "patch", patchFallback: true });
201
- await expect(pending).resolves.toBe(true);
202
- expect(events[1]).toEqual([
203
- "onBundleDownloaded",
204
- {
205
- channel: CHANNEL,
206
- fromBundleId: CURRENT_BUNDLE_ID,
207
- fromReleaseId: null,
208
- toBundleId: TARGET_BUNDLE_ID,
209
- toReleaseId: RELEASE_ID,
210
- updateStrategy: "appVersion",
211
- delivery: "patch",
212
- patchFallback: true,
213
- },
214
- ]);
215
- });
216
-
217
- it.each(["nothing new", "failure"])(
218
- "does not report a download when staging brings %s",
219
- async (result) => {
220
- const { checkForUpdate } = await import("./checkForUpdate");
221
- const events = await recordPluginEvents();
222
- const { client } = createClient();
223
- if (result === "nothing new")
224
- mocks.stageBundle.mockResolvedValueOnce(null);
225
- else
226
- mocks.stageBundle.mockRejectedValueOnce(new Error("download failed"));
227
- const update = await checkForUpdate({
228
- client,
229
- updateStrategy: "appVersion",
230
- });
231
- await update!.updateBundle().catch(() => false);
232
- expect(events.map(([name]) => name)).not.toContain("onBundleDownloaded");
233
- },
234
- );
235
-
236
- it("selects locally and defers Bundle artifact resolution until install", async () => {
237
- const { checkForUpdate } = await import("./checkForUpdate");
238
- const { client, fetchReleaseCatalog, resolveArtifact } = createClient();
239
-
240
- const result = await checkForUpdate({
241
- client,
242
- updateStrategy: "appVersion",
243
- });
244
-
245
- expect(result).toMatchObject({
246
- id: RELEASE_ID,
247
- releaseId: RELEASE_ID,
248
- transitionKind: "INSTALL",
249
- });
250
- expect(fetchReleaseCatalog).toHaveBeenCalledWith({
251
- appVersion: "1.2.0",
252
- channel: CHANNEL,
253
- fingerprintHash: null,
254
- platform: "ios",
255
- requestHeaders: undefined,
256
- requestTimeout: undefined,
257
- updateStrategy: "appVersion",
258
- });
259
- expect(resolveArtifact).not.toHaveBeenCalled();
260
-
261
- await expect(result?.updateBundle()).resolves.toBe(true);
262
-
263
- expect(resolveArtifact).toHaveBeenCalledWith({
264
- currentBundleId: CURRENT_BUNDLE_ID,
265
- requestHeaders: undefined,
266
- requestTimeout: undefined,
267
- targetBundleId: TARGET_BUNDLE_ID,
268
- });
269
- expect(mocks.stageBundle).toHaveBeenCalledWith(
270
- expect.objectContaining({
271
- bundleId: TARGET_BUNDLE_ID,
272
- archiveUrl: "https://updates.example.com/bundle.tar.br",
273
- selection: expect.objectContaining({
274
- catalogId: CATALOG_ID,
275
- releaseId: RELEASE_ID,
276
- scopeKey: SCOPE_KEY,
277
- }),
278
- }),
279
- );
280
- });
281
-
282
- it("adopts a newer Release for the same Bundle without resolving bytes", async () => {
283
- const active: PersistedSelectionReceipt = {
284
- catalogId: CATALOG_ID,
285
- bundleId: TARGET_BUNDLE_ID,
286
- catalogHash: `sha256:${"b".repeat(64)}`,
287
- channel: CHANNEL,
288
- generation: 1,
289
- kind: "BUNDLE",
290
- releaseId: MINIMUM_RELEASE_ID,
291
- scopeKey: SCOPE_KEY,
292
- selectionContextHash: "old-context",
293
- };
294
- mocks.getBundleId.mockReturnValueOnce(TARGET_BUNDLE_ID);
295
- mocks.getActiveUpdateState.mockReturnValueOnce({
296
- activeSelection: active,
297
- highestSeenCatalogs: {},
298
- stableSelection: active,
299
- verificationPending: false,
300
- });
301
- const { checkForUpdate } = await import("./checkForUpdate");
302
- const forceCatalog = createCatalog({
303
- releases: [
304
- {
305
- ...createCatalog().releases[0]!,
306
- shouldForceUpdate: true,
307
- },
308
- ],
309
- });
310
- const events = await recordPluginEvents();
311
- const { client, resolveArtifact } = createClient(forceCatalog);
312
-
313
- const result = await checkForUpdate({
314
- client,
315
- updateStrategy: "appVersion",
316
- });
317
-
318
- expect(result).toMatchObject({
319
- id: RELEASE_ID,
320
- shouldForceUpdate: false,
321
- transitionKind: "ADOPT_RELEASE",
322
- });
323
- await expect(result?.updateBundle()).resolves.toBe(true);
324
- expect(resolveArtifact).not.toHaveBeenCalled();
325
- expect(mocks.stageBundle).not.toHaveBeenCalled();
326
- expect(events.map(([name]) => name)).not.toContain("onBundleDownloaded");
327
- expect(mocks.commitReleaseSelection).toHaveBeenCalledWith({
328
- guard: expect.objectContaining({ generation: 2, scopeKey: SCOPE_KEY }),
329
- selection: expect.objectContaining({
330
- bundleId: TARGET_BUNDLE_ID,
331
- releaseId: RELEASE_ID,
332
- }),
333
- });
334
- });
335
-
336
- it("reports a same-file adoption as UNCHANGED with the adopted Release", async () => {
337
- const active: PersistedSelectionReceipt = {
338
- catalogId: CATALOG_ID,
339
- bundleId: TARGET_BUNDLE_ID,
340
- catalogHash: `sha256:${"b".repeat(64)}`,
341
- channel: CHANNEL,
342
- generation: 1,
343
- kind: "BUNDLE",
344
- releaseId: MINIMUM_RELEASE_ID,
345
- scopeKey: SCOPE_KEY,
346
- selectionContextHash: "old-context",
347
- };
348
- mocks.getBundleId.mockReturnValueOnce(TARGET_BUNDLE_ID);
349
- mocks.getActiveUpdateState.mockReturnValueOnce({
350
- activeSelection: active,
351
- highestSeenCatalogs: {},
352
- stableSelection: active,
353
- verificationPending: false,
354
- });
355
- const { checkForUpdate } = await import("./checkForUpdate");
356
- const events = await recordPluginEvents();
357
- const { client } = createClient();
358
-
359
- const result = await checkForUpdate({
360
- client,
361
- updateStrategy: "appVersion",
362
- });
363
- await result?.updateBundle();
364
-
365
- expect(events.at(-1)).toEqual([
366
- "onUpdateCheck",
367
- {
368
- status: "UNCHANGED",
369
- channel: CHANNEL,
370
- bundleId: TARGET_BUNDLE_ID,
371
- releaseId: RELEASE_ID,
372
- previousReleaseId: MINIMUM_RELEASE_ID,
373
- },
374
- ]);
375
- });
376
-
377
- it("selects a lower-id Release when explicitly switching scopes", async () => {
378
- const betaChannel = "beta";
379
- const betaScopeKey = createReleaseCatalogScopeKey({
380
- channelKey: encodeChannelKey(betaChannel),
381
- platform: "ios",
382
- strategy: "APP_VERSION",
383
- });
384
- const active: PersistedSelectionReceipt = {
385
- catalogId: CATALOG_ID,
386
- bundleId: CURRENT_BUNDLE_ID,
387
- catalogHash: `sha256:${"b".repeat(64)}`,
388
- channel: CHANNEL,
389
- generation: 1,
390
- kind: "BUNDLE",
391
- releaseId: ACTIVE_RELEASE_ID,
392
- scopeKey: SCOPE_KEY,
393
- selectionContextHash: "old-context",
394
- };
395
- mocks.getActiveUpdateState.mockReturnValueOnce({
396
- activeSelection: active,
397
- highestSeenCatalogs: {},
398
- stableSelection: active,
399
- verificationPending: false,
400
- });
401
- const betaCatalog = createCatalog({ scopeKey: betaScopeKey });
402
- const { checkForUpdate } = await import("./checkForUpdate");
403
- const { client } = createClient(betaCatalog);
404
-
405
- const result = await checkForUpdate({
406
- channel: betaChannel,
407
- client,
408
- updateStrategy: "appVersion",
409
- });
410
-
411
- expect(result).toMatchObject({
412
- id: RELEASE_ID,
413
- releaseId: RELEASE_ID,
414
- status: "UPDATE",
415
- transitionKind: "INSTALL",
416
- });
417
- });
418
-
419
- it("refreshes the same Release receipt when catalog provenance changes", async () => {
420
- const active: PersistedSelectionReceipt = {
421
- catalogId: CATALOG_ID,
422
- bundleId: TARGET_BUNDLE_ID,
423
- catalogHash: `sha256:${"b".repeat(64)}`,
424
- channel: CHANNEL,
425
- generation: 1,
426
- kind: "BUNDLE",
427
- releaseId: RELEASE_ID,
428
- scopeKey: SCOPE_KEY,
429
- selectionContextHash: "old-context",
430
- };
431
- mocks.getBundleId.mockReturnValueOnce(TARGET_BUNDLE_ID);
432
- mocks.getActiveUpdateState.mockReturnValueOnce({
433
- activeSelection: active,
434
- highestSeenCatalogs: {},
435
- stableSelection: active,
436
- verificationPending: false,
437
- });
438
- const { checkForUpdate } = await import("./checkForUpdate");
439
- const { client, resolveArtifact } = createClient();
440
-
441
- const result = await checkForUpdate({
442
- client,
443
- updateStrategy: "appVersion",
444
- });
445
-
446
- expect(result).toMatchObject({
447
- id: RELEASE_ID,
448
- releaseId: RELEASE_ID,
449
- transitionKind: "ADOPT_RELEASE",
450
- });
451
- await expect(result?.updateBundle()).resolves.toBe(true);
452
- expect(resolveArtifact).not.toHaveBeenCalled();
453
- expect(mocks.stageBundle).not.toHaveBeenCalled();
454
- expect(mocks.commitReleaseSelection).toHaveBeenCalledWith({
455
- guard: expect.objectContaining({
456
- catalogHash: CATALOG_HASH,
457
- generation: 2,
458
- scopeKey: SCOPE_KEY,
459
- }),
460
- selection: expect.objectContaining({
461
- bundleId: TARGET_BUNDLE_ID,
462
- catalogHash: CATALOG_HASH,
463
- generation: 2,
464
- releaseId: RELEASE_ID,
465
- }),
466
- });
467
- });
468
-
469
- it("rejects a slow artifact completion after a newer catalog wins", async () => {
470
- mocks.isReleaseSelectionCurrent
471
- .mockReturnValueOnce(true)
472
- .mockReturnValueOnce(false);
473
- const { StaleReleaseCatalogError } = await import("./error");
474
- const { checkForUpdate } = await import("./checkForUpdate");
475
- const { client } = createClient();
476
- const result = await checkForUpdate({
477
- client,
478
- updateStrategy: "appVersion",
479
- });
480
-
481
- await expect(result?.updateBundle()).rejects.toBeInstanceOf(
482
- StaleReleaseCatalogError,
483
- );
484
- expect(mocks.stageBundle).not.toHaveBeenCalled();
485
- });
486
-
487
- it("installs an older enabled Release as a forced rollback", async () => {
488
- const active: PersistedSelectionReceipt = {
489
- catalogId: CATALOG_ID,
490
- bundleId: CURRENT_BUNDLE_ID,
491
- catalogHash: `sha256:${"b".repeat(64)}`,
492
- channel: CHANNEL,
493
- generation: 1,
494
- kind: "BUNDLE",
495
- releaseId: ACTIVE_RELEASE_ID,
496
- scopeKey: SCOPE_KEY,
497
- selectionContextHash: "old-context",
498
- };
499
- mocks.getActiveUpdateState.mockReturnValueOnce({
500
- activeSelection: active,
501
- highestSeenCatalogs: {},
502
- stableSelection: active,
503
- verificationPending: false,
504
- });
505
- const catalog = createCatalog({
506
- rollbackReleases: [
507
- {
508
- bundleId: TARGET_BUNDLE_ID,
509
- kind: "BUNDLE",
510
- message: "Previous Release",
511
- releaseId: RELEASE_ID,
512
- rolloutCohortCount: 0,
513
- shouldForceUpdate: false,
514
- targetCohorts: [],
515
- },
516
- ],
517
- });
518
- const { checkForUpdate } = await import("./checkForUpdate");
519
- const { client } = createClient(catalog);
520
-
521
- const result = await checkForUpdate({
522
- client,
523
- updateStrategy: "appVersion",
524
- });
525
-
526
- expect(result).toMatchObject({
527
- id: RELEASE_ID,
528
- releaseId: RELEASE_ID,
529
- shouldForceUpdate: true,
530
- status: "ROLLBACK",
531
- transitionKind: "INSTALL",
532
- });
533
- await expect(result?.updateBundle()).resolves.toBe(true);
534
- expect(mocks.stageBundle).toHaveBeenCalledWith(
535
- expect.objectContaining({
536
- bundleId: TARGET_BUNDLE_ID,
537
- status: "ROLLBACK",
538
- }),
539
- );
540
- });
541
-
542
- it("uses current Bundle identity when migrating an unauthenticated receipt", async () => {
543
- const migratedCurrentBundleId = "00000000-0000-7001-8000-000000000003";
544
- const active: PersistedSelectionReceipt = {
545
- catalogId: null,
546
- bundleId: migratedCurrentBundleId,
547
- catalogHash: null,
548
- channel: CHANNEL,
549
- generation: null,
550
- kind: "BUNDLE",
551
- releaseId: null,
552
- scopeKey: null,
553
- selectionContextHash: null,
554
- };
555
- mocks.getBundleId.mockReturnValueOnce(migratedCurrentBundleId);
556
- mocks.getActiveUpdateState.mockReturnValueOnce({
557
- activeSelection: active,
558
- highestSeenCatalogs: {},
559
- stableSelection: active,
560
- verificationPending: false,
561
- });
562
- const catalog = createCatalog({
563
- releases: [],
564
- rollbackReleases: [
565
- {
566
- ...createCatalog().releases[0]!,
567
- rolloutCohortCount: 0,
568
- },
569
- ],
570
- });
571
- const { checkForUpdate } = await import("./checkForUpdate");
572
- const { client } = createClient(catalog);
573
-
574
- const result = await checkForUpdate({
575
- client,
576
- updateStrategy: "appVersion",
577
- });
578
-
579
- expect(result).toMatchObject({
580
- id: RELEASE_ID,
581
- shouldForceUpdate: true,
582
- status: "ROLLBACK",
583
- transitionKind: "INSTALL",
584
- });
585
- });
586
-
587
- it.each(["EMBEDDED", "BUILTIN"] as const)(
588
- "returns a forced %s rollback with its console ID or built-in fallback",
589
- async (kind) => {
590
- const active: PersistedSelectionReceipt = {
591
- catalogId: CATALOG_ID,
592
- bundleId: CURRENT_BUNDLE_ID,
593
- catalogHash: `sha256:${"b".repeat(64)}`,
594
- channel: CHANNEL,
595
- generation: 1,
596
- kind: "BUNDLE",
597
- releaseId: MINIMUM_RELEASE_ID,
598
- scopeKey: SCOPE_KEY,
599
- selectionContextHash: "old-context",
600
- };
601
- mocks.getActiveUpdateState.mockReturnValueOnce({
602
- activeSelection: active,
603
- highestSeenCatalogs: {},
604
- stableSelection: active,
605
- verificationPending: false,
606
- });
607
- const { checkForUpdate } = await import("./checkForUpdate");
608
- const { client, resolveArtifact } = createClient(
609
- createCatalog({
610
- releases:
611
- kind === "EMBEDDED"
612
- ? [{ ...createCatalog().releases[0]!, kind, bundleId: null }]
613
- : [],
614
- rollbackReleases: [],
615
- }),
616
- );
617
-
618
- const result = await checkForUpdate({
619
- client,
620
- updateStrategy: "appVersion",
621
- });
622
-
623
- expect(result).toMatchObject({
624
- id: kind === "EMBEDDED" ? RELEASE_ID : MINIMUM_RELEASE_ID,
625
- releaseId: kind === "EMBEDDED" ? RELEASE_ID : null,
626
- shouldForceUpdate: true,
627
- status: "ROLLBACK",
628
- transitionKind: kind === "EMBEDDED" ? "USE_EMBEDDED" : "USE_BUILTIN",
629
- });
630
- await expect(result?.updateBundle()).resolves.toBe(true);
631
- expect(mocks.commitReleaseSelection).toHaveBeenCalledWith(
632
- expect.objectContaining({
633
- selection: expect.objectContaining({
634
- bundleId: MINIMUM_RELEASE_ID,
635
- kind,
636
- releaseId: kind === "EMBEDDED" ? RELEASE_ID : null,
637
- }),
638
- }),
639
- );
640
- expect(resolveArtifact).not.toHaveBeenCalled();
641
- expect(mocks.stageBundle).not.toHaveBeenCalled();
642
- },
643
- );
644
-
645
- it("does not surface a rollback when the app already uses built-in bytes", async () => {
646
- mocks.getBundleId.mockReturnValueOnce(MINIMUM_RELEASE_ID);
647
- const { checkForUpdate } = await import("./checkForUpdate");
648
- const { client } = createClient(
649
- createCatalog({ releases: [], rollbackReleases: [] }),
650
- );
651
-
652
- await expect(
653
- checkForUpdate({ client, updateStrategy: "appVersion" }),
654
- ).resolves.toBeNull();
655
- expect(mocks.commitReleaseSelection).not.toHaveBeenCalled();
656
- });
657
-
658
- it("reports a rejected generation without selecting or resolving artifacts", async () => {
659
- mocks.acceptReleaseCatalog.mockReturnValueOnce(false);
660
- const onError = vi.fn();
661
- const { checkForUpdate } = await import("./checkForUpdate");
662
- const { client, resolveArtifact } = createClient();
663
-
664
- await expect(
665
- checkForUpdate({ client, onError, updateStrategy: "appVersion" }),
666
- ).resolves.toBeNull();
667
-
668
- expect(onError).toHaveBeenCalledWith(
669
- expect.objectContaining({
670
- message: "Rejected a stale or inconsistent Release catalog",
671
- }),
672
- );
673
- expect(resolveArtifact).not.toHaveBeenCalled();
674
- });
675
-
676
- it("rejects mismatched catalog scope before native catalog state mutates", async () => {
677
- const wrongScope = createReleaseCatalogScopeKey({
678
- channelKey: encodeChannelKey("beta"),
679
- platform: "ios",
680
- strategy: "APP_VERSION",
681
- });
682
- const onError = vi.fn();
683
- const { checkForUpdate } = await import("./checkForUpdate");
684
- const { client } = createClient(createCatalog({ scopeKey: wrongScope }));
685
-
686
- await expect(
687
- checkForUpdate({ client, onError, updateStrategy: "appVersion" }),
688
- ).resolves.toBeNull();
689
-
690
- expect(onError).toHaveBeenCalledWith(
691
- expect.objectContaining({
692
- message: "Received an invalid Release catalog",
693
- }),
694
- );
695
- expect(mocks.acceptReleaseCatalog).not.toHaveBeenCalled();
696
- expect(mocks.commitReleaseSelection).not.toHaveBeenCalled();
697
- expect(mocks.stageBundle).not.toHaveBeenCalled();
698
- });
699
-
700
- it("rejects an unexpected Catalog identity before accepting catalog state", async () => {
701
- const active: PersistedSelectionReceipt = {
702
- catalogId: "existing-project",
703
- bundleId: CURRENT_BUNDLE_ID,
704
- catalogHash: `sha256:${"b".repeat(64)}`,
705
- channel: CHANNEL,
706
- generation: 1,
707
- kind: "BUNDLE",
708
- releaseId: ACTIVE_RELEASE_ID,
709
- scopeKey: createReleaseCatalogScopeKey({
710
- channelKey: encodeChannelKey(CHANNEL),
711
- platform: "ios",
712
- strategy: "APP_VERSION",
713
- }),
714
- selectionContextHash: "old-context",
715
- };
716
- mocks.getActiveUpdateState.mockReturnValueOnce({
717
- activeSelection: active,
718
- highestSeenCatalogs: {},
719
- stableSelection: active,
720
- verificationPending: false,
721
- });
722
- const onError = vi.fn();
723
- const { checkForUpdate } = await import("./checkForUpdate");
724
- const { client } = createClient();
725
-
726
- await expect(
727
- checkForUpdate({ client, onError, updateStrategy: "appVersion" }),
728
- ).resolves.toBeNull();
729
-
730
- expect(onError).toHaveBeenCalledWith(
731
- expect.objectContaining({
732
- message: "Release transition rejected: UNSOLICITED_SCOPE",
733
- }),
734
- );
735
- expect(mocks.acceptReleaseCatalog).not.toHaveBeenCalled();
736
- });
737
- it("reports a check that found nothing to install as UNCHANGED", async () => {
738
- mocks.getBundleId.mockReturnValueOnce(MINIMUM_RELEASE_ID);
739
- const { checkForUpdate } = await import("./checkForUpdate");
740
- const events = await recordPluginEvents();
741
- const { client } = createClient(
742
- createCatalog({ releases: [], rollbackReleases: [] }),
743
- );
744
-
745
- await expect(
746
- checkForUpdate({ client, updateStrategy: "appVersion" }),
747
- ).resolves.toBeNull();
748
-
749
- expect(events).toEqual([
750
- [
751
- "onUpdateCheck",
752
- {
753
- status: "UNCHANGED",
754
- channel: CHANNEL,
755
- bundleId: MINIMUM_RELEASE_ID,
756
- releaseId: null,
757
- previousReleaseId: null,
758
- },
759
- ],
760
- ]);
761
- });
762
-
763
- it("treats a scope without a catalog as no update, not a failure", async () => {
764
- const onError = vi.fn();
765
- const { checkForUpdate } = await import("./checkForUpdate");
766
- const events = await recordPluginEvents();
767
- const { client, fetchReleaseCatalog } = createClient();
768
- fetchReleaseCatalog.mockResolvedValueOnce(null as never);
769
-
770
- await expect(
771
- checkForUpdate({ client, onError, updateStrategy: "appVersion" }),
772
- ).resolves.toBeNull();
773
-
774
- expect(onError).not.toHaveBeenCalled();
775
- expect(mocks.acceptReleaseCatalog).not.toHaveBeenCalled();
776
- expect(events).toEqual([
777
- [
778
- "onUpdateCheck",
779
- expect.objectContaining({ status: "UNCHANGED", channel: CHANNEL }),
780
- ],
781
- ]);
782
- });
783
-
784
- it.each([
785
- {
786
- label: "an HTTP status",
787
- error: () => new UpdateHttpError(503, "Service Unavailable"),
788
- expected: { reason: "http", httpStatus: 503 },
789
- },
790
- {
791
- label: "no response",
792
- error: () => new TypeError("Network request failed"),
793
- expected: { reason: "network" },
794
- },
795
- {
796
- label: "a 404 without the no-catalog mark",
797
- error: () => new UpdateHttpError(404, "Not Found"),
798
- expected: { reason: "http", httpStatus: 404 },
799
- },
800
- {
801
- label: "a timeout",
802
- error: () => new Error("Request timed out"),
803
- expected: { reason: "network", transport: "timeout" },
804
- },
805
- {
806
- label: "an invalid catalog",
807
- error: () =>
808
- new InvalidUpdateResponseError("Received an invalid Release catalog"),
809
- expected: { reason: "invalid_response" },
810
- },
811
- ])("reports a check failed by $label", async ({ error, expected }) => {
812
- const { checkForUpdate } = await import("./checkForUpdate");
813
- const events = await recordPluginEvents();
814
- const { client, fetchReleaseCatalog } = createClient();
815
- const cause = error();
816
- fetchReleaseCatalog.mockRejectedValueOnce(cause);
817
-
818
- await expect(
819
- checkForUpdate({ client, updateStrategy: "appVersion" }),
820
- ).resolves.toBeNull();
821
-
822
- expect(events).toEqual([
823
- [
824
- "onUpdateError",
825
- {
826
- stage: "check",
827
- resource: "catalog",
828
- ...expected,
829
- channel: CHANNEL,
830
- bundleId: CURRENT_BUNDLE_ID,
831
- releaseId: null,
832
- updateStrategy: "appVersion",
833
- cause,
834
- },
835
- ],
836
- ]);
837
- });
838
-
839
- it.each([
840
- {
841
- label: "an artifact request answered 500",
842
- fail: (resolveArtifact: ReturnType<typeof vi.fn>) =>
843
- resolveArtifact.mockRejectedValueOnce(
844
- new UpdateHttpError(500, "Internal Server Error"),
845
- ),
846
- expected: {
847
- stage: "download",
848
- reason: "http",
849
- resource: "artifact",
850
- httpStatus: 500,
851
- },
852
- },
853
- {
854
- label: "native extraction",
855
- fail: () =>
856
- mocks.stageBundle.mockRejectedValueOnce(
857
- Object.assign(new Error("Failed to extract archive"), {
858
- code: "UNKNOWN_ERROR",
859
- userInfo: { reason: "extract", stage: "install" },
860
- }),
861
- ),
862
- expected: { stage: "install", reason: "extract" },
863
- },
864
- {
865
- label: "a native HTTP status",
866
- fail: () =>
867
- mocks.stageBundle.mockRejectedValueOnce(
868
- Object.assign(new Error("Failed to download bundle"), {
869
- code: "DOWNLOAD_FAILED",
870
- userInfo: {
871
- httpStatus: 403,
872
- originCode: "ExpiredToken",
873
- reason: "http",
874
- resource: "archive",
875
- stage: "download",
876
- },
877
- }),
878
- ),
879
- expected: {
880
- stage: "download",
881
- reason: "http",
882
- resource: "archive",
883
- httpStatus: 403,
884
- originCode: "ExpiredToken",
885
- },
886
- },
887
- {
888
- label: "a native connection loss",
889
- fail: () =>
890
- mocks.stageBundle.mockRejectedValueOnce(
891
- Object.assign(new Error("The network connection was lost."), {
892
- code: "DOWNLOAD_FAILED",
893
- userInfo: {
894
- reason: "network",
895
- resource: "file",
896
- stage: "download",
897
- transport: "connection",
898
- },
899
- }),
900
- ),
901
- expected: {
902
- stage: "download",
903
- reason: "network",
904
- resource: "file",
905
- transport: "connection",
906
- },
907
- },
908
- ])(
909
- "reports an install failed by $label and rethrows it",
910
- async ({ fail, expected }) => {
911
- const { checkForUpdate } = await import("./checkForUpdate");
912
- const events = await recordPluginEvents();
913
- const { client, resolveArtifact } = createClient();
914
- fail(resolveArtifact);
915
- const result = await checkForUpdate({
916
- client,
917
- updateStrategy: "appVersion",
918
- });
919
-
920
- await expect(result!.updateBundle()).rejects.toThrow();
921
-
922
- expect(events.filter(([name]) => name === "onUpdateError")).toEqual([
923
- [
924
- "onUpdateError",
925
- expect.objectContaining({
926
- ...expected,
927
- channel: CHANNEL,
928
- bundleId: CURRENT_BUNDLE_ID,
929
- targetBundleId: TARGET_BUNDLE_ID,
930
- targetReleaseId: RELEASE_ID,
931
- updateStrategy: "appVersion",
932
- }),
933
- ],
934
- ]);
935
- },
936
- );
937
-
938
- it.each([
939
- {
940
- label: "a native rejection without a failure class",
941
- fail: () =>
942
- mocks.stageBundle.mockRejectedValueOnce(
943
- Object.assign(new Error("Release catalog selection is stale"), {
944
- code: "UNKNOWN_ERROR",
945
- }),
946
- ),
947
- },
948
- {
949
- label: "a selection that became stale",
950
- fail: () => mocks.isReleaseSelectionCurrent.mockReturnValueOnce(false),
951
- },
952
- ])("does not report $label as an update failure", async ({ fail }) => {
953
- const { checkForUpdate } = await import("./checkForUpdate");
954
- const events = await recordPluginEvents();
955
- const { client } = createClient();
956
- const result = await checkForUpdate({
957
- client,
958
- updateStrategy: "appVersion",
959
- });
960
- fail();
961
-
962
- await expect(result!.updateBundle()).rejects.toThrow();
963
-
964
- expect(events.map(([name]) => name)).toEqual(["onUpdateCheck"]);
965
- });
966
- });