@hot-updater/test-utils 1.0.0-rc.15 → 1.0.0-rc.16

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/README.md CHANGED
@@ -1,64 +1,86 @@
1
1
  # @hot-updater/test-utils
2
2
 
3
- Shared Vitest conformance tests for custom Hot Updater database providers and
4
- HTTP servers. Official providers and example servers run these same scenarios.
3
+ Vitest suites for Hot Updater database adapters, providers and servers. The
4
+ official providers and example servers run these same suites.
5
5
 
6
6
  ```bash
7
7
  pnpm add -D @hot-updater/test-utils @hot-updater/server vitest
8
8
  ```
9
9
 
10
- Connect your database lifecycle and the real server:
10
+ ## Database adapters and providers
11
+
12
+ Register three suites against a disposable test database:
13
+
14
+ - `setupDatabaseAdapterConformanceSuite` runs the storage adapter contract case
15
+ by case: value round-trips, ordering, guards, all-or-nothing writes, unique
16
+ and multi-valued indexes, paging under capped native pages, and concurrent
17
+ writers.
18
+ - `setupReadBudgetTestSuite` checks what each read of core and the built-in
19
+ plugins costs at the adapter, using `createMeasuredDatabase` from
20
+ `@hot-updater/server/db`. `postgresRowsExamined` and `mysqlRowsExamined` also
21
+ count the rows a SQL database examined.
22
+ - `setupDatabaseTestSuite` runs core, Bundles, the Release Catalog contract and
23
+ Insights through the admin and client HTTP APIs of `createHotUpdater`.
24
+
25
+ Connect your database's lifecycle and the real server:
11
26
 
12
27
  ```ts
13
28
  import { createHotUpdater } from "@hot-updater/server";
29
+ import { createDatabasePluginApis } from "@hot-updater/server/db";
30
+ import {
31
+ createInsightsModel,
32
+ insights,
33
+ } from "@hot-updater/server/plugins/insights";
14
34
  import {
15
- setupDatabasePluginTestSuite,
35
+ setupDatabaseTestSuite,
16
36
  startHttpTestServer,
17
37
  } from "@hot-updater/test-utils";
18
- import { myDatabase } from "./myDatabase";
19
- import { db, migrateDatabase, resetDatabase } from "./testDatabase";
20
38
 
21
- setupDatabasePluginTestSuite({
39
+ import { migrateMyDatabase, myDatabase } from "./myDatabase";
40
+ import { resetMyDatabase } from "./testDatabase";
41
+
42
+ const config = { connectionString: process.env.TEST_DATABASE_URL };
43
+
44
+ setupDatabaseTestSuite({
22
45
  name: "my database",
23
- createPlugin: () => myDatabase({ db }),
24
- migrate: () => migrateDatabase(db),
25
- reset: () => resetDatabase(db),
26
- dispose: async (plugin) => { await plugin.dispose?.(); },
27
- createHttpClient: (options) => startHttpTestServer(
28
- createHotUpdater({ ...options, clientAccess: { type: "public" } }).handlers,
29
- ),
46
+ migrate: () => migrateMyDatabase(config),
47
+ createDatabase: () => myDatabase(config),
48
+ reset: () => resetMyDatabase(config),
49
+ dispose: async (database) => {
50
+ await database.dispose?.();
51
+ },
52
+ createHttpClient: (options) =>
53
+ startHttpTestServer(
54
+ createHotUpdater({
55
+ ...options,
56
+ plugins: [insights()],
57
+ clientAccess: "public",
58
+ }).handlers,
59
+ ),
60
+ createInsightsModel: (database) =>
61
+ createInsightsModel(
62
+ createDatabasePluginApis(database, [insights()]).insights,
63
+ ),
30
64
  });
31
65
  ```
32
66
 
33
- Run the spec with `pnpm exec vitest run`. Use an isolated test database; reset
34
- all data before each scenario while retaining its schema and version settings.
35
-
36
- Passing the complete suite verifies the DatabasePlugin and Release Catalog
37
- HTTP contracts for the installed Hot Updater version: model semantics,
38
- transactions, pagination, relations, Insights, catalog projections and caching,
39
- and artifact responses. Lifecycle scenarios feed those HTTP catalogs into the
40
- production client selector and advance the simulated device after checking the
41
- selected artifact: built-in → OTA A → OTA B → rollback A → built-in. They also
42
- exercise deleted Releases, the native minimum bundle, compatibility, cohort
43
- changes, and crash history. Native download, activation, and restart remain
44
- device E2E responsibilities.
45
-
46
- The HTTP scenarios use real `fetch` requests on Node,
47
- including independent Release/Catalog expectations and competing conditional
48
- writes. Run the entire suite against your provider's actual test backend without
49
- skipping scenarios or replacing model methods with mocks.
67
+ `migrate` creates the tables and schema settings once. `reset` empties every
68
+ data table before each test and keeps the settings rows. Run the suites
69
+ against your actual backend without skipping cases or mocking the database.
70
+ The Release Catalog scenarios feed each catalog they receive to the production
71
+ client selector and keep a simulated device's state across updates, rollbacks
72
+ and the built-in fallback. Native download, activation and restart remain
73
+ device end-to-end responsibilities.
50
74
 
51
- The repository also runs the public suite against deliberately broken providers
52
- to check that contract violations fail. This is a versioned compatibility gate;
53
- passing is evidence for the exercised contracts, not a proof of every possible
54
- execution or deployment condition.
55
- Add provider-specific migration, concurrency, storage, and deployment tests
56
- where those need separate verification.
75
+ ## Plugins and servers
57
76
 
58
- Use `setupReleaseCatalogTestSuite` with `createHttpTestClient` to test an existing
59
- server by URL. Use `createHandlerHttpTestClient` for a Workers Vitest pool.
60
- Server processes can import `createReleaseCatalogTestStorage` from
61
- `@hot-updater/test-utils/node` to install the suite's storage fixtures.
77
+ - `createPluginTestHarness` runs one server plugin the way `createHotUpdater`
78
+ does, on a memory adapter by default, and measures what each API call reads.
79
+ - `setupReleaseCatalogTestSuite` with `createHttpTestClient` tests a running
80
+ server by URL. Use `createHandlerHttpTestClient` inside a Workers Vitest pool.
81
+ - Server processes can import `createReleaseCatalogTestStorage` from
82
+ `@hot-updater/test-utils/node` to install the suites' storage fixtures.
62
83
 
63
- See the [custom database guide](https://hot-updater.dev/docs/database-plugins/custom-database)
64
- for the full setup and contract boundaries.
84
+ See the
85
+ [custom database adapter guide](https://hot-updater.dev/docs/database-plugins/custom-database)
86
+ for the adapter contract and complete specs for each suite.
package/dist/index.d.mts CHANGED
@@ -289,8 +289,8 @@ interface ReadBudgetServer {
289
289
  readonly plugins: readonly unknown[];
290
290
  /** `targetBaseCandidateKey` from `@hot-updater/server/db`. */
291
291
  targetBaseCandidateKey(target: {
292
- readonly channelId: string;
293
- readonly platform: string;
292
+ readonly channel: string;
293
+ readonly platform: "ios" | "android";
294
294
  readonly fingerprintHash: string | null;
295
295
  readonly appVersion: string | null;
296
296
  }): string | null;
package/dist/index.mjs CHANGED
@@ -880,6 +880,25 @@ const registerInsightsModelTests = (state) => {
880
880
  bundle: [predicate, predicate]
881
881
  }), 1);
882
882
  });
883
+ it("counts a latest installation once when a from and a to predicate both match it", async () => {
884
+ const model = state.getDatabase();
885
+ const moved = createBundleEventRowFixture("9701", 100);
886
+ await record(model, moved);
887
+ await expectInsightsIndex(() => model.countLatestEvents({
888
+ platform: moved.platform,
889
+ channel: moved.channel,
890
+ sinceMs: 0,
891
+ bundle: [{
892
+ field: "from_bundle_id",
893
+ value: moved.from_bundle_id,
894
+ types: [moved.type]
895
+ }, {
896
+ field: "to_bundle_id",
897
+ value: moved.to_bundle_id,
898
+ types: [moved.type]
899
+ }]
900
+ }), 1);
901
+ });
883
902
  it("returns zero for successful empty scalar queries", async () => {
884
903
  const model = state.getDatabase();
885
904
  await expect(model.countLatestEvents({
@@ -2886,7 +2905,7 @@ const seed = async (database, server) => {
2886
2905
  nightlyId: (await channel("nightly")).id,
2887
2906
  /** The auto-patch base key of a new production iOS 1.0.0 bundle. */
2888
2907
  candidateKey: server.targetBaseCandidateKey({
2889
- channelId: productionId,
2908
+ channel: "production",
2890
2909
  platform: "ios",
2891
2910
  fingerprintHash: null,
2892
2911
  appVersion: "1.0.0"
@@ -3006,12 +3025,12 @@ const READ_BUDGETS = [
3006
3025
  returned: (releases) => releases.length
3007
3026
  }),
3008
3027
  budget({
3009
- api: "deploy base candidates: at most maxBaseBundles rows",
3028
+ api: "deploy base candidates: 1 catalog point read",
3010
3029
  read: ({ core, candidateKey }) => core.findBaseBundleIds(candidateKey, createBundleFixture("107").id, 2),
3011
- adapter: reads(0, 0, 1, 2),
3030
+ adapter: reads(1, 1, 0, 0),
3012
3031
  engine: {
3013
3032
  calls: 1,
3014
- rows: 2
3033
+ rows: 1
3015
3034
  },
3016
3035
  check: (ids) => expect(ids).toEqual(["106", "105"].map((suffix) => createBundleFixture(suffix).id))
3017
3036
  }),
@@ -3035,17 +3054,47 @@ const READ_BUDGETS = [
3035
3054
  returned: (channel) => channel === null ? 0 : 1
3036
3055
  }),
3037
3056
  budget({
3038
- api: "list events: limit rows, and one zero-row query per empty day",
3057
+ api: "list events of a dense day: limit rows from one query",
3058
+ read: ({ insights }) => insights.listEvents({
3059
+ filter: { kind: "all" },
3060
+ sinceMs: T0 - DAY,
3061
+ beforeReceivedAtMs: T0,
3062
+ limit: 5
3063
+ }),
3064
+ adapter: reads(0, 0, 1, 5),
3065
+ engine: {
3066
+ calls: 1,
3067
+ rows: 5
3068
+ },
3069
+ returned: (events) => events.length
3070
+ }),
3071
+ budget({
3072
+ api: "list events across a gap: limit rows, one empty day, and one outcome row",
3039
3073
  read: ({ insights }) => insights.listEvents({
3040
3074
  filter: bundleEvents,
3041
3075
  sinceMs: T0,
3042
3076
  beforeReceivedAtMs: T0 + 3 * DAY,
3043
3077
  limit: 5
3044
3078
  }),
3045
- adapter: reads(0, 0, 3, 5),
3079
+ adapter: reads(1, 7, 4, 6),
3046
3080
  engine: {
3047
- calls: 3,
3048
- rows: 5
3081
+ calls: 4,
3082
+ rows: 6
3083
+ },
3084
+ returned: (events) => events.length + 1
3085
+ }),
3086
+ budget({
3087
+ api: "list events over an empty range: its top day and one outcome read, both empty",
3088
+ read: ({ insights }) => insights.listEvents({
3089
+ filter: { kind: "all" },
3090
+ sinceMs: T0 - 91 * DAY,
3091
+ beforeReceivedAtMs: T0 - DAY,
3092
+ limit: 5
3093
+ }),
3094
+ adapter: reads(0, 0, 2, 0),
3095
+ engine: {
3096
+ calls: 2,
3097
+ rows: 0
3049
3098
  },
3050
3099
  returned: (events) => events.length
3051
3100
  }),
@@ -3179,7 +3228,7 @@ const READ_BUDGETS = [
3179
3228
  bundle: bundleOf("107"),
3180
3229
  release: production
3181
3230
  }]),
3182
- adapter: (copies) => reads(copies ? 6 : 4, copies ? 9 : 4, 3, 6),
3231
+ adapter: (copies) => reads(copies ? 5 : 3, copies ? 8 : 3, 3, 6),
3183
3232
  engine: {
3184
3233
  calls: 6,
3185
3234
  rows: 9
@@ -3213,7 +3262,7 @@ const READ_BUDGETS = [
3213
3262
  install_id: "install-2",
3214
3263
  received_at_ms: T0 + 3 * HOUR
3215
3264
  })),
3216
- adapter: reads(5, 19, 0, 0),
3265
+ adapter: reads(5, 21, 0, 0),
3217
3266
  engine: {
3218
3267
  calls: 2,
3219
3268
  rows: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/test-utils",
3
- "version": "1.0.0-rc.15",
3
+ "version": "1.0.0-rc.16",
4
4
  "type": "module",
5
5
  "description": "Test utilities for Hot Updater packages",
6
6
  "sideEffects": false,
@@ -38,7 +38,7 @@
38
38
  "execa": "*",
39
39
  "vitest": "^4.0.0",
40
40
  "@hot-updater/core": "^1.0.0-rc.15",
41
- "@hot-updater/plugin-core": "^1.0.0-rc.15"
41
+ "@hot-updater/plugin-core": "^1.0.0-rc.16"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^20",
@@ -47,9 +47,9 @@
47
47
  "typescript": "npm:@typescript/typescript6@6.0.2",
48
48
  "vitest": "4.1.4",
49
49
  "workspace-tools": "^0.41.7",
50
- "@hot-updater/plugin-core": "1.0.0-rc.15",
51
- "@hot-updater/js": "1.0.0-rc.14",
52
- "@hot-updater/core": "1.0.0-rc.15"
50
+ "@hot-updater/core": "1.0.0-rc.15",
51
+ "@hot-updater/plugin-core": "1.0.0-rc.16",
52
+ "@hot-updater/js": "1.0.0-rc.14"
53
53
  },
54
54
  "inlinedDependencies": {
55
55
  "@nodelib/fs.scandir": "2.1.5",