@hot-updater/test-utils 1.0.0-rc.27 → 1.0.0-rc.29

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.
package/dist/index.mjs CHANGED
@@ -3285,6 +3285,27 @@ const READ_BUDGETS = [
3285
3285
  installations: 24
3286
3286
  }])
3287
3287
  }),
3288
+ budget({
3289
+ api: "daily bundle share: only scoped aggregate shards in the requested days",
3290
+ read: ({ insights }) => insights.getDistributionHistory({
3291
+ channel: "production",
3292
+ platform: "ios",
3293
+ timeRange: {
3294
+ start: T0,
3295
+ end: T0 + 3 * DAY
3296
+ }
3297
+ }),
3298
+ adapter: reads(0, 0, 1, 15),
3299
+ engine: {
3300
+ calls: 1,
3301
+ rows: 2
3302
+ },
3303
+ check: ({ points }) => expect(points.map(({ bundles }) => bundles.reduce((n, row) => n + row.installations, 0))).toEqual([
3304
+ 24,
3305
+ 0,
3306
+ 1
3307
+ ])
3308
+ }),
3288
3309
  budget({
3289
3310
  api: "update failures of a release over a window: buckets × shards, all used",
3290
3311
  read: ({ insights }) => insights.getUpdateFailures({
@@ -3388,10 +3409,10 @@ const READ_BUDGETS = [
3388
3409
  install_id: "install-2",
3389
3410
  received_at_ms: T0 + 3 * HOUR
3390
3411
  })),
3391
- adapter: reads(4, 7, 0, 0),
3412
+ adapter: reads(5, 8, 0, 0),
3392
3413
  engine: {
3393
- calls: 2,
3394
- rows: 1
3414
+ calls: 3,
3415
+ rows: 2
3395
3416
  },
3396
3417
  writes: 1
3397
3418
  })
@@ -4486,6 +4507,34 @@ const registerInsightsModelTests = (state) => {
4486
4507
  failedLaunches: 1,
4487
4508
  uniqueUsers: 2
4488
4509
  });
4510
+ const hourly = await model.getReleaseActivity({
4511
+ releases: [releases[1]],
4512
+ timeRange: {
4513
+ start: 0,
4514
+ end: 108e5
4515
+ },
4516
+ intervalMs: 36e5
4517
+ });
4518
+ expect(hourly.data[0]?.metrics.series).toEqual([
4519
+ {
4520
+ startMs: 0,
4521
+ downloads: 1,
4522
+ launches: 2,
4523
+ failedLaunches: 1
4524
+ },
4525
+ {
4526
+ startMs: 36e5,
4527
+ downloads: 0,
4528
+ launches: 0,
4529
+ failedLaunches: 0
4530
+ },
4531
+ {
4532
+ startMs: 72e5,
4533
+ downloads: 0,
4534
+ launches: 0,
4535
+ failedLaunches: 0
4536
+ }
4537
+ ]);
4489
4538
  const usage = await model.getAppUsage({
4490
4539
  channel: "production",
4491
4540
  platform: "all",
@@ -4512,6 +4561,109 @@ const registerInsightsModelTests = (state) => {
4512
4561
  installations: 1
4513
4562
  }]));
4514
4563
  });
4564
+ it("keeps one daily running-bundle observation through updates, recovery, retries, and later days", async () => {
4565
+ const model = state.getDatabase();
4566
+ const day = 864e5;
4567
+ const start = Math.floor(Date.now() / day) * day - 3 * day;
4568
+ const a = "00000000-0000-7000-8000-000000008001";
4569
+ const b = "00000000-0000-7000-8000-000000008002";
4570
+ const download = {
4571
+ ...createBundleEventRowFixture("9901", start + 100),
4572
+ type: "UPDATE_DOWNLOADED",
4573
+ install_id: "daily-one",
4574
+ from_release_id: a,
4575
+ to_release_id: b
4576
+ };
4577
+ const applied = {
4578
+ ...download,
4579
+ type: "UPDATE_APPLIED",
4580
+ id: createBundleEventRowFixture("9902", 0).id,
4581
+ received_at_ms: start + 200
4582
+ };
4583
+ const read = () => model.getDistributionHistory({
4584
+ platform: "ios",
4585
+ channel: "production",
4586
+ timeRange: {
4587
+ start,
4588
+ end: start + 3 * day
4589
+ }
4590
+ });
4591
+ await record(model, download);
4592
+ await expectInsightsIndex(async () => (await read()).points[0].bundles, [{
4593
+ appVersion: "1.0.0",
4594
+ releaseId: a,
4595
+ bundleKind: "release",
4596
+ installations: 1
4597
+ }]);
4598
+ for (const row of [
4599
+ applied,
4600
+ applied,
4601
+ {
4602
+ ...applied,
4603
+ id: createBundleEventRowFixture("9903", 0).id,
4604
+ install_id: "daily-two"
4605
+ }
4606
+ ]) await record(model, row);
4607
+ const recovered = {
4608
+ ...applied,
4609
+ id: createBundleEventRowFixture("9904", 0).id,
4610
+ type: "RECOVERED",
4611
+ from_release_id: b,
4612
+ to_release_id: a,
4613
+ received_at_ms: start + 300
4614
+ };
4615
+ await record(model, recovered);
4616
+ await record(model, {
4617
+ ...applied,
4618
+ id: createBundleEventRowFixture("9905", 0).id,
4619
+ received_at_ms: start + day + 100
4620
+ });
4621
+ await record(model, {
4622
+ ...applied,
4623
+ id: createBundleEventRowFixture("9906", 0).id,
4624
+ received_at_ms: start + 250
4625
+ });
4626
+ await record(model, {
4627
+ ...applied,
4628
+ id: createBundleEventRowFixture("9907", 0).id,
4629
+ install_id: "other-scope",
4630
+ channel: "preview"
4631
+ });
4632
+ await expectInsightsIndex(async () => {
4633
+ return (await read()).points.map((point) => ({
4634
+ ...point,
4635
+ bundles: [...point.bundles].sort((x, y) => (x.releaseId ?? "").localeCompare(y.releaseId ?? ""))
4636
+ }));
4637
+ }, [
4638
+ {
4639
+ startMs: start,
4640
+ bundles: [{
4641
+ appVersion: "1.0.0",
4642
+ releaseId: a,
4643
+ bundleKind: "release",
4644
+ installations: 1
4645
+ }, {
4646
+ appVersion: "1.0.0",
4647
+ releaseId: b,
4648
+ bundleKind: "release",
4649
+ installations: 1
4650
+ }]
4651
+ },
4652
+ {
4653
+ startMs: start + day,
4654
+ bundles: [{
4655
+ appVersion: "1.0.0",
4656
+ releaseId: b,
4657
+ bundleKind: "release",
4658
+ installations: 1
4659
+ }]
4660
+ },
4661
+ {
4662
+ startMs: start + 2 * day,
4663
+ bundles: []
4664
+ }
4665
+ ]);
4666
+ });
4515
4667
  it("pages insights events newest first with a stable cursor", async () => {
4516
4668
  const model = state.getDatabase();
4517
4669
  const first = createBundleEventRowFixture("701", 100);
@@ -4713,6 +4865,132 @@ const setupInsightsHttpTestSuite = (options) => {
4713
4865
  return (await (await client.admin(`/overview?platform=ios&channel=${encodeURIComponent(channel)}&window=24h`)).json()).reportingInstallations.count;
4714
4866
  }, 1);
4715
4867
  });
4868
+ it("serves daily observed bundles through the authenticated admin API", async () => {
4869
+ const client = options.getClient();
4870
+ const channel = `share-${crypto.randomUUID()}`;
4871
+ const day = 864e5;
4872
+ const start = Math.floor(Date.now() / day) * day;
4873
+ const event = {
4874
+ appVersion: "1.0.0",
4875
+ channel,
4876
+ cohort: "default",
4877
+ fingerprintHash: null,
4878
+ fromBundleId: null,
4879
+ fromReleaseId: null,
4880
+ installId: `share-${crypto.randomUUID()}`,
4881
+ platform: "ios",
4882
+ toBundleId: "00000000-0000-0000-0000-000000000000",
4883
+ toReleaseId: null,
4884
+ type: "UNCHANGED",
4885
+ updateStrategy: null
4886
+ };
4887
+ const reported = await client.client("/events", jsonRequest$1("POST", event));
4888
+ expect(reported.status).toBe(204);
4889
+ await reported.text();
4890
+ const path = `/distribution-history?platform=ios&channel=${channel}&start=${start}&end=${start + day}`;
4891
+ await expectInsightsIndex(async () => {
4892
+ const response = await client.admin(path);
4893
+ expect(response.status).toBe(200);
4894
+ return (await response.json()).points;
4895
+ }, [{
4896
+ startMs: start,
4897
+ bundles: [{
4898
+ appVersion: "1.0.0",
4899
+ releaseId: null,
4900
+ bundleKind: "builtin",
4901
+ installations: 1
4902
+ }]
4903
+ }]);
4904
+ const invalid = await client.admin(`/distribution-history?platform=ios&channel=${channel}&start=${start}&end=${start + 40 * day}`);
4905
+ expect(invalid.status).toBe(400);
4906
+ await invalid.text();
4907
+ });
4908
+ it.each([
4909
+ {
4910
+ type: "UNCHANGED",
4911
+ status: 200,
4912
+ body: "{\"releases\":[]}"
4913
+ },
4914
+ {
4915
+ type: "UNCHANGED",
4916
+ status: 304,
4917
+ body: ""
4918
+ },
4919
+ {
4920
+ type: "UPDATE_FAILED",
4921
+ status: 503,
4922
+ body: "{\"error\":\"Database unavailable\"}"
4923
+ },
4924
+ {
4925
+ type: "UPDATE_DOWNLOADED",
4926
+ status: 200,
4927
+ body: "{\"artifactProtocolVersion\":1}"
4928
+ },
4929
+ {
4930
+ type: "UPDATE_APPLIED",
4931
+ status: 200,
4932
+ body: "{}"
4933
+ },
4934
+ {
4935
+ type: "RECOVERED",
4936
+ status: 502,
4937
+ body: null
4938
+ }
4939
+ ])("keeps HTTP $status diagnostics on an existing $type report", async ({ type, status, body }) => {
4940
+ const client = options.getClient();
4941
+ const installId = `install-${crypto.randomUUID()}`;
4942
+ const channel = `http-${crypto.randomUUID()}`;
4943
+ const bundleId = "00000000-0000-7000-8000-000000000001";
4944
+ const httpResponse = {
4945
+ resource: "catalog",
4946
+ path: "/release-catalogs/app-version/ios/production/1.0.0",
4947
+ status,
4948
+ body,
4949
+ bodyTruncated: false,
4950
+ receivedAtMs: Date.now() - 1e3
4951
+ };
4952
+ const event = {
4953
+ eventId: "01929f4e-2b7c-7a51-9d3e-5c1f0a6b8e21",
4954
+ type,
4955
+ installId,
4956
+ platform: "ios",
4957
+ appVersion: "1.0.0",
4958
+ channel,
4959
+ cohort: "1",
4960
+ fingerprintHash: null,
4961
+ sdkVersion: "1.0.0",
4962
+ fromBundleId: type === "UNCHANGED" ? null : bundleId,
4963
+ toBundleId: bundleId,
4964
+ fromReleaseId: null,
4965
+ toReleaseId: null,
4966
+ updateStrategy: type === "UNCHANGED" ? null : "appVersion",
4967
+ metadata: {
4968
+ httpResponse,
4969
+ ...type === "UPDATE_FAILED" ? { failure: {
4970
+ stage: "check",
4971
+ reason: "http",
4972
+ httpStatus: status
4973
+ } } : {}
4974
+ }
4975
+ };
4976
+ for (let attempt = 0; attempt < 2; attempt++) {
4977
+ const response = await client.client("/events", jsonRequest$1("POST", event));
4978
+ expect(response.status).toBe(204);
4979
+ await response.text();
4980
+ }
4981
+ const installation = await client.admin(`/installations/${encodeURIComponent(installId)}`);
4982
+ if (type === "UPDATE_FAILED") expect(installation.status).toBe(404);
4983
+ else expect(await installation.json()).toMatchObject({
4984
+ latestStatus: type,
4985
+ httpResponse
4986
+ });
4987
+ const data = (await (await client.admin(`/installations/${encodeURIComponent(installId)}/events`)).json()).data;
4988
+ expect(data).toHaveLength(type === "UNCHANGED" ? 0 : 1);
4989
+ if (type !== "UNCHANGED") expect(data[0]).toMatchObject({
4990
+ type,
4991
+ httpResponse
4992
+ });
4993
+ });
4716
4994
  it("records an update failure in installation history and the failures read", async () => {
4717
4995
  const client = options.getClient();
4718
4996
  const installId = `install-${crypto.randomUUID()}`;
@@ -154,7 +154,8 @@ const setupClientPlugins = (plugins, options = {}) => {
154
154
  onAppReady: call("onAppReady"),
155
155
  onUpdateCheck: call("onUpdateCheck"),
156
156
  onBundleDownloaded: call("onBundleDownloaded"),
157
- onUpdateError: call("onUpdateError")
157
+ onUpdateError: call("onUpdateError"),
158
+ onHttpResponse: call("onHttpResponse")
158
159
  },
159
160
  listens: (name) => host.hasPluginHook(name),
160
161
  requests,
@@ -1,4 +1,4 @@
1
- import { AppReadyResult, BundleDownloadedInfo, HotUpdaterBaseURL, HotUpdaterClientPlugin, UpdateCheckResult, UpdateError } from "@hot-updater/protocol";
1
+ import { AppReadyResult, BundleDownloadedInfo, HotUpdaterBaseURL, HotUpdaterClientPlugin, UpdateCheckResult, UpdateError, UpdateHttpResponse } from "@hot-updater/protocol";
2
2
  //#region src/setupClientPlugins.d.ts
3
3
  /** A value the test fixes, or reads each time a plugin asks for it. */
4
4
  type TestValue<T> = T | (() => T);
@@ -68,6 +68,7 @@ interface ClientPluginTestHooks {
68
68
  onUpdateCheck(result: UpdateCheckResult): void;
69
69
  onBundleDownloaded(info: BundleDownloadedInfo): void;
70
70
  onUpdateError(error: UpdateError): void;
71
+ onHttpResponse(response: UpdateHttpResponse): void;
71
72
  }
72
73
  /** One JavaScript runtime with the plugins set up, as after `HotUpdater.init`. */
73
74
  interface ClientPluginTestRuntime {
@@ -1,4 +1,4 @@
1
- import { AppReadyResult, BundleDownloadedInfo, HotUpdaterBaseURL, HotUpdaterClientPlugin, UpdateCheckResult, UpdateError } from "@hot-updater/protocol";
1
+ import { AppReadyResult, BundleDownloadedInfo, HotUpdaterBaseURL, HotUpdaterClientPlugin, UpdateCheckResult, UpdateError, UpdateHttpResponse } from "@hot-updater/protocol";
2
2
  //#region src/setupClientPlugins.d.ts
3
3
  /** A value the test fixes, or reads each time a plugin asks for it. */
4
4
  type TestValue<T> = T | (() => T);
@@ -68,6 +68,7 @@ interface ClientPluginTestHooks {
68
68
  onUpdateCheck(result: UpdateCheckResult): void;
69
69
  onBundleDownloaded(info: BundleDownloadedInfo): void;
70
70
  onUpdateError(error: UpdateError): void;
71
+ onHttpResponse(response: UpdateHttpResponse): void;
71
72
  }
72
73
  /** One JavaScript runtime with the plugins set up, as after `HotUpdater.init`. */
73
74
  interface ClientPluginTestRuntime {
@@ -153,7 +153,8 @@ const setupClientPlugins = (plugins, options = {}) => {
153
153
  onAppReady: call("onAppReady"),
154
154
  onUpdateCheck: call("onUpdateCheck"),
155
155
  onBundleDownloaded: call("onBundleDownloaded"),
156
- onUpdateError: call("onUpdateError")
156
+ onUpdateError: call("onUpdateError"),
157
+ onHttpResponse: call("onHttpResponse")
157
158
  },
158
159
  listens: (name) => host.hasPluginHook(name),
159
160
  requests,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/test-utils",
3
- "version": "1.0.0-rc.27",
3
+ "version": "1.0.0-rc.29",
4
4
  "type": "module",
5
5
  "description": "Test utilities for Hot Updater packages",
6
6
  "sideEffects": false,
@@ -50,10 +50,10 @@
50
50
  "mysql2": "*",
51
51
  "pg": "*",
52
52
  "vitest": "^4.0.0",
53
- "@hot-updater/protocol": "^1.0.0-rc.27",
54
- "@hot-updater/plugin-insights": "^1.0.0-rc.27",
55
- "@hot-updater/plugin-core": "^1.0.0-rc.27",
56
- "@hot-updater/server": "^1.0.0-rc.27"
53
+ "@hot-updater/plugin-insights": "^1.0.0-rc.30",
54
+ "@hot-updater/server": "^1.0.0-rc.30",
55
+ "@hot-updater/protocol": "^1.0.0-rc.28",
56
+ "@hot-updater/plugin-core": "^1.0.0-rc.28"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@electric-sql/pglite": {
@@ -83,11 +83,11 @@
83
83
  "typescript": "npm:@typescript/typescript6@6.0.2",
84
84
  "vitest": "4.1.4",
85
85
  "workspace-tools": "^0.41.7",
86
- "@hot-updater/plugin-insights": "1.0.0-rc.27",
87
- "@hot-updater/protocol": "1.0.0-rc.27",
86
+ "@hot-updater/plugin-insights": "1.0.0-rc.30",
88
87
  "@hot-updater/js": "1.0.0-rc.27",
89
- "@hot-updater/server": "1.0.0-rc.27",
90
- "@hot-updater/plugin-core": "1.0.0-rc.27"
88
+ "@hot-updater/protocol": "1.0.0-rc.28",
89
+ "@hot-updater/plugin-core": "1.0.0-rc.28",
90
+ "@hot-updater/server": "1.0.0-rc.30"
91
91
  },
92
92
  "inlinedDependencies": {
93
93
  "@nodelib/fs.scandir": "2.1.5",