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

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,94 @@
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 and the Release Catalog contract
23
+ through the admin and client HTTP APIs of `createHotUpdater`, then the suites
24
+ of the server plugins listed in `plugins`, such as the Insights plugin's
25
+ `insightsTestSuite()` from `@hot-updater/server/plugins/insights/testing`.
26
+
27
+ Connect your database's lifecycle and the real server:
11
28
 
12
29
  ```ts
13
30
  import { createHotUpdater } from "@hot-updater/server";
31
+ import { createDatabasePluginApis } from "@hot-updater/server/db";
32
+ import {
33
+ createInsightsModel,
34
+ insights,
35
+ } from "@hot-updater/server/plugins/insights";
36
+ import { insightsTestSuite } from "@hot-updater/server/plugins/insights/testing";
14
37
  import {
15
- setupDatabasePluginTestSuite,
38
+ setupDatabaseTestSuite,
16
39
  startHttpTestServer,
17
40
  } from "@hot-updater/test-utils";
18
- import { myDatabase } from "./myDatabase";
19
- import { db, migrateDatabase, resetDatabase } from "./testDatabase";
20
41
 
21
- setupDatabasePluginTestSuite({
42
+ import { migrateMyDatabase, myDatabase } from "./myDatabase";
43
+ import { resetMyDatabase } from "./testDatabase";
44
+
45
+ const config = { connectionString: process.env.TEST_DATABASE_URL };
46
+
47
+ setupDatabaseTestSuite({
22
48
  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
- ),
49
+ migrate: () => migrateMyDatabase(config),
50
+ createDatabase: () => myDatabase(config),
51
+ reset: () => resetMyDatabase(config),
52
+ dispose: async (database) => {
53
+ await database.dispose?.();
54
+ },
55
+ createHttpClient: (options) =>
56
+ startHttpTestServer(
57
+ createHotUpdater({
58
+ ...options,
59
+ plugins: [insights()],
60
+ clientAccess: "public",
61
+ }).handlers,
62
+ ),
63
+ // The server above runs insights(), so its suite runs too.
64
+ plugins: [
65
+ insightsTestSuite({
66
+ createModel: (database) =>
67
+ createInsightsModel(
68
+ createDatabasePluginApis(database, [insights()]).insights,
69
+ ),
70
+ }),
71
+ ],
30
72
  });
31
73
  ```
32
74
 
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.
75
+ `migrate` creates the tables and schema settings once. `reset` empties every
76
+ data table before each test and keeps the settings rows. Run the suites
77
+ against your actual backend without skipping cases or mocking the database.
78
+ The Release Catalog scenarios feed each catalog they receive to the production
79
+ client selector and keep a simulated device's state across updates, rollbacks
80
+ and the built-in fallback. Native download, activation and restart remain
81
+ device end-to-end responsibilities.
50
82
 
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.
83
+ ## Plugins and servers
57
84
 
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.
85
+ - `createPluginTestHarness` runs one server plugin the way `createHotUpdater`
86
+ does, on a memory adapter by default, and measures what each API call reads.
87
+ - `setupReleaseCatalogTestSuite` with `createHttpTestClient` tests a running
88
+ server by URL. Use `createHandlerHttpTestClient` inside a Workers Vitest pool.
89
+ - Server processes can import `createReleaseCatalogTestStorage` from
90
+ `@hot-updater/test-utils/node` to install the suites' storage fixtures.
62
91
 
63
- See the [custom database guide](https://hot-updater.dev/docs/database-plugins/custom-database)
64
- for the full setup and contract boundaries.
92
+ See the
93
+ [custom database adapter guide](https://hot-updater.dev/docs/database-plugins/custom-database)
94
+ for the adapter contract and complete specs for each suite.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ArtifactInfo, Bundle, ReleaseCatalog } from "@hot-updater/core";
2
2
  import { DatabaseAdapter, DatabaseReadCount, PhysicalTable } from "@hot-updater/plugin-core/internal";
3
- import { BundleRow, ChannelRow, DeployReleasePolicy, EngineDatabase, HotUpdaterCoreApi, InsightsModel, ReleaseRow, StoragePlugin } from "@hot-updater/plugin-core";
3
+ import { AggregateBatching, BundleRow, ChannelRow, DeployReleasePolicy, EngineDatabase, HotUpdaterCoreApi, ReleaseRow, StoragePlugin } from "@hot-updater/plugin-core";
4
4
  //#region src/httpTestClient.d.ts
5
5
  type HttpTestRequestInit = NonNullable<ConstructorParameters<typeof Request>[1]>;
6
6
  type HttpTestRequest = (path: string, init?: HttpTestRequestInit) => Promise<Response>;
@@ -71,37 +71,6 @@ declare const setupCoreAdminTestSuite: (options: {
71
71
  readonly getClient: () => HttpTestClient;
72
72
  }) => void;
73
73
  //#endregion
74
- //#region src/setupInsightsHttpTestSuite.d.ts
75
- /**
76
- * The Insights plugin's routes on a database: a client reports an event, and
77
- * the admin routes read it back. The server must run `insights()`.
78
- */
79
- declare const setupInsightsHttpTestSuite: (options: {
80
- readonly getClient: () => HttpTestClient;
81
- }) => void;
82
- //#endregion
83
- //#region src/databaseTestRunner.d.ts
84
- type Awaitable<T> = Promise<T> | T;
85
- /** How a suite gets a provider's database, and gives it back. */
86
- type DatabaseTestLifecycle<TDatabase> = {
87
- readonly name: string;
88
- /** Creates the provider's tables and settings rows, before `createDatabase`. */
89
- readonly migrate: () => Awaitable<void>;
90
- /** The provider's database, as its factory returns it. */
91
- readonly createDatabase: () => Awaitable<TDatabase>;
92
- /** Empties every table between tests. */
93
- readonly reset: (database: TDatabase) => Awaitable<void>;
94
- readonly dispose: (database: TDatabase) => Awaitable<void>;
95
- };
96
- //#endregion
97
- //#region src/setupInsightsModelTestSuite.d.ts
98
- /**
99
- * The Insights report contract on one database: pass the Insights plugin's
100
- * model over it, `createInsightsModel(createDatabasePluginApis(database,
101
- * [insights()]).insights)`, as `createDatabase`.
102
- */
103
- declare const setupInsightsModelTestSuite: (lifecycle: DatabaseTestLifecycle<InsightsModel>) => void;
104
- //#endregion
105
74
  //#region src/setupSemverSatisfiesTestSuite.d.ts
106
75
  /**
107
76
  *
@@ -124,29 +93,50 @@ declare const setupSemverSatisfiesTestSuite: ({ semverSatisfies }: {
124
93
  semverSatisfies: (targetAppVersion: string, currentVersion: string) => Promise<boolean> | boolean;
125
94
  }) => void;
126
95
  //#endregion
96
+ //#region src/databaseTestRunner.d.ts
97
+ type Awaitable<T> = Promise<T> | T;
98
+ /** How a suite gets a provider's database, and gives it back. */
99
+ type DatabaseTestLifecycle<TDatabase> = {
100
+ readonly name: string;
101
+ /** Creates the provider's tables and settings rows, before `createDatabase`. */
102
+ readonly migrate: () => Awaitable<void>;
103
+ /** The provider's database, as its factory returns it. */
104
+ readonly createDatabase: () => Awaitable<TDatabase>;
105
+ /** Empties every table between tests. */
106
+ readonly reset: (database: TDatabase) => Awaitable<void>;
107
+ readonly dispose: (database: TDatabase) => Awaitable<void>;
108
+ };
109
+ //#endregion
127
110
  //#region src/setupDatabaseTestSuite.d.ts
111
+ /**
112
+ * A server plugin's tests on a provider's database, such as the Insights
113
+ * plugin's `insightsTestSuite()`. The provider's HTTP server must run the
114
+ * plugin.
115
+ */
116
+ interface DatabasePluginTestSuite<TDatabase = unknown> {
117
+ readonly name: string;
118
+ register(context: {
119
+ readonly getClient: () => HttpTestClient;
120
+ readonly getDatabase: () => TDatabase;
121
+ }): void;
122
+ }
128
123
  type DatabaseTestSuiteOptions<TDatabase extends EngineDatabase = EngineDatabase> = DatabaseTestLifecycle<TDatabase> & {
129
124
  /**
130
- * Serves the handlers of `createHotUpdater({ database, storage, plugins:
131
- * [insights()], clientAccess: "public" })` over HTTP: in process, or in the
132
- * runtime the provider deploys to.
125
+ * Serves the handlers of `createHotUpdater({ database, storage, plugins,
126
+ * clientAccess: "public" })` over HTTP, in process or in the runtime the
127
+ * provider deploys to, with the plugins whose suites `plugins` lists.
133
128
  */
134
129
  readonly createHttpClient: (options: {
135
130
  readonly database: TDatabase;
136
131
  readonly storage: readonly StoragePlugin[];
137
132
  }) => HttpTestServer | Promise<HttpTestServer>;
138
- /**
139
- * The Insights plugin's model on the database, to run the Insights report
140
- * contract on its tables: `(database) => createInsightsModel(
141
- * createDatabasePluginApis(database, [insights()]).insights)`.
142
- */
143
- readonly createInsightsModel?: (database: TDatabase) => InsightsModel;
133
+ /** The suites of the server plugins the provider opts in to test. */
134
+ readonly plugins?: readonly DatabasePluginTestSuite<TDatabase>[];
144
135
  };
145
136
  /**
146
137
  * A provider's database under Hot Updater's server: core's operations, the
147
- * Release Catalog contract, bundles, and Insights, all through HTTP, on the
148
- * tables the provider's tooling creates; with `createInsightsModel`, also the
149
- * Insights report contract in process.
138
+ * Release Catalog contract, and bundles, all through HTTP, on the tables the
139
+ * provider's tooling creates, then each opted-in plugin's suite.
150
140
  */
151
141
  declare const setupDatabaseTestSuite: <TDatabase extends EngineDatabase>(options: DatabaseTestSuiteOptions<TDatabase>) => void;
152
142
  //#endregion
@@ -167,9 +157,17 @@ interface DatabaseAdapterConformanceOptions {
167
157
  readonly maxOps?: number;
168
158
  /** Concurrent writers in the contention cases (default 32). */
169
159
  readonly writers?: number;
160
+ /**
161
+ * How the backend deletes rows past a table's retention: `prune` (the
162
+ * default) through the adapter, or `ttl`, natively on every item, with no
163
+ * `prune` on the adapter.
164
+ */
165
+ readonly retention?: "prune" | "ttl";
170
166
  }
171
167
  declare const conformanceItems: PhysicalTable;
172
168
  declare const conformanceCounters: PhysicalTable;
169
+ /** Rows expire 1,000 ms after `at`; a null `at` never expires. */
170
+ declare const conformanceExpiring: PhysicalTable;
173
171
  /**
174
172
  * The storage-adapter conformance contract (PRD S3 and S4 cases). Every
175
173
  * adapter runs it through `verifyAdapter`, which also checks each read and
@@ -281,16 +279,18 @@ interface ReadBudgetServer {
281
279
  readonly now: () => number;
282
280
  readonly storage: ReadBudgetStorage;
283
281
  }): ReadBudgetDatabase;
284
- /** `builtInSchema` from `@hot-updater/server/database`: the tables to create. */
285
- readonly builtInSchema: {
286
- readonly tables: readonly PhysicalTable[];
282
+ /** `toolingTargetOf` from `@hot-updater/server/database`: core's and the plugins' tables to create. */
283
+ toolingTargetOf(plugins: readonly unknown[]): {
284
+ readonly schema: {
285
+ readonly tables: readonly PhysicalTable[];
286
+ };
287
287
  };
288
- /** `[insights(), apiKeys()]`, the built-in plugins whose reads have budgets. */
288
+ /** `[insights(), apiKeys()]`, the plugins whose reads have budgets. */
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;
@@ -325,6 +325,51 @@ interface ReadBudgetSuiteOptions {
325
325
  */
326
326
  declare const setupReadBudgetTestSuite: (options: ReadBudgetSuiteOptions) => void;
327
327
  //#endregion
328
+ //#region src/setupAggregateBatchingTestSuite.d.ts
329
+ interface Module {
330
+ readonly id: string;
331
+ readonly schema: object;
332
+ }
333
+ /**
334
+ * What the suite takes from `@hot-updater/server/database`; pass that
335
+ * module, which keeps test-utils free of a dependency on the server.
336
+ */
337
+ interface AggregateBatchingEngine {
338
+ resolveSchema(modules: readonly Module[]): {
339
+ readonly tables: readonly PhysicalTable[];
340
+ };
341
+ readonly aggregateBatchingModule: Module;
342
+ createDatabaseEngine(options: {
343
+ readonly adapter: DatabaseAdapter;
344
+ readonly schema: never;
345
+ readonly batching?: AggregateBatching;
346
+ readonly retry?: {
347
+ readonly attempts?: number;
348
+ };
349
+ }): {
350
+ database(module: Module): unknown;
351
+ flush(): Promise<void>;
352
+ };
353
+ }
354
+ interface AggregateBatchingSuiteOptions {
355
+ readonly name: string;
356
+ /** `import * as engine from "@hot-updater/server/database"`. */
357
+ readonly engine: AggregateBatchingEngine;
358
+ /** Returns an adapter over an empty store; each case calls it once. */
359
+ readonly createAdapter: () => Promise<{
360
+ readonly adapter: DatabaseAdapter;
361
+ readonly cleanup?: () => Promise<void>;
362
+ }>;
363
+ /** Aggregate rows one transaction changes in the too-large case: more than one write takes. */
364
+ readonly oversizedRows: number;
365
+ }
366
+ /**
367
+ * Aggregates declared `batched` on a key-value store: log rows the store
368
+ * writes with each transaction and a compaction merges once, and a memory
369
+ * buffer, read back exactly as transactional writes read.
370
+ */
371
+ declare const setupAggregateBatchingTestSuite: ({ name, engine, createAdapter, oversizedRows }: AggregateBatchingSuiteOptions) => void;
372
+ //#endregion
328
373
  //#region src/runContentionHarness.d.ts
329
374
  /** Delays every adapter call by `ms`, like a round trip to a remote database. */
330
375
  declare const withAdapterLatency: (adapter: DatabaseAdapter, ms: number) => DatabaseAdapter;
@@ -385,6 +430,8 @@ interface PluginTestEngine {
385
430
  interface HarnessPlugin {
386
431
  readonly id: string;
387
432
  readonly schema: object;
433
+ /** `false` keeps the declared table names, as `createHotUpdater` does. */
434
+ readonly namespace?: false;
388
435
  init(context: never): {
389
436
  readonly api: unknown;
390
437
  };
@@ -449,4 +496,4 @@ declare const RELEASE_CATALOG_STORAGE_FIXTURES: Readonly<Record<string, string>>
449
496
  /** Storage boundary shared by provider and separately spawned server tests. */
450
497
  declare const createReleaseCatalogTestStorage: () => StoragePlugin;
451
498
  //#endregion
452
- export { AdminApiTestClient, ContentionOptions, ContentionReport, DatabaseAdapterConformanceContext, DatabaseAdapterConformanceOptions, type DatabaseTestLifecycle, DatabaseTestSuiteOptions, HttpTestClient, HttpTestHandlers, HttpTestRequest, HttpTestRequestInit, HttpTestServer, PluginTestEngine, PluginTestHarnessOptions, RELEASE_CATALOG_DOWNLOAD_HASH, RELEASE_CATALOG_MANIFEST_URIS, RELEASE_CATALOG_STORAGE_FIXTURES, ReadBudgetAdapterContext, ReadBudgetDatabase, ReadBudgetServer, ReadBudgetStorage, ReadBudgetSuiteOptions, RowsExamined, SqlExecutorLike, SqlQuery, SqlStatementLike, conformanceCounters, conformanceItems, createAdminApiTestClient, createHandlerHttpTestClient, createHttpTestClient, createPluginTestHarness, createReleaseCatalogTestStorage, jsonRequest, mysqlRowsExamined, postgresRowsExamined, releaseCatalogArtifact, releaseCatalogDownloadUrl, runContentionHarness, setupBundleMethodsTestSuite, setupCoreAdminTestSuite, setupDatabaseAdapterConformanceSuite, setupDatabaseTestSuite, setupInsightsHttpTestSuite, setupInsightsModelTestSuite, setupReadBudgetTestSuite, setupReleaseCatalogTestSuite, setupSemverSatisfiesTestSuite, startHttpTestServer, toDeployBundle, withAdapterLatency };
499
+ export { AdminApiTestClient, AggregateBatchingEngine, AggregateBatchingSuiteOptions, ContentionOptions, ContentionReport, DatabaseAdapterConformanceContext, DatabaseAdapterConformanceOptions, DatabasePluginTestSuite, type DatabaseTestLifecycle, DatabaseTestSuiteOptions, HttpTestClient, HttpTestHandlers, HttpTestRequest, HttpTestRequestInit, HttpTestServer, PluginTestEngine, PluginTestHarnessOptions, RELEASE_CATALOG_DOWNLOAD_HASH, RELEASE_CATALOG_MANIFEST_URIS, RELEASE_CATALOG_STORAGE_FIXTURES, ReadBudgetAdapterContext, ReadBudgetDatabase, ReadBudgetServer, ReadBudgetStorage, ReadBudgetSuiteOptions, RowsExamined, SqlExecutorLike, SqlQuery, SqlStatementLike, conformanceCounters, conformanceExpiring, conformanceItems, createAdminApiTestClient, createHandlerHttpTestClient, createHttpTestClient, createPluginTestHarness, createReleaseCatalogTestStorage, jsonRequest, mysqlRowsExamined, postgresRowsExamined, releaseCatalogArtifact, releaseCatalogDownloadUrl, runContentionHarness, setupAggregateBatchingTestSuite, setupBundleMethodsTestSuite, setupCoreAdminTestSuite, setupDatabaseAdapterConformanceSuite, setupDatabaseTestSuite, setupReadBudgetTestSuite, setupReleaseCatalogTestSuite, setupSemverSatisfiesTestSuite, startHttpTestServer, toDeployBundle, withAdapterLatency };