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

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,40 +1,49 @@
1
1
  # @hot-updater/test-utils
2
2
 
3
- Vitest suites for Hot Updater database adapters, providers and servers. The
4
- official providers and example servers run these same suites.
3
+ Test suites and helpers for Hot Updater storage adapters, database adapters,
4
+ providers, server plugins, client plugins and servers. The official adapters,
5
+ providers, plugins and example servers run these same suites.
5
6
 
6
7
  ```bash
7
8
  pnpm add -D @hot-updater/test-utils @hot-updater/server vitest
8
9
  ```
9
10
 
11
+ It uses the other Hot Updater packages only through their public API, and
12
+ takes them as peer dependencies. `@hot-updater/server` and
13
+ `@hot-updater/plugin-insights` are optional: the database suites load the
14
+ server when they run, and the Insights suites use the plugin's types. The SQL
15
+ test executors of `@hot-updater/test-utils/node` take `pg`, `mysql2` or
16
+ `@electric-sql/pglite`, whichever you test on.
17
+
10
18
  ## Database adapters and providers
11
19
 
12
20
  Register three suites against a disposable test database:
13
21
 
14
- - `setupDatabaseAdapterConformanceSuite` runs the storage adapter contract case
22
+ - `setupDatabaseAdapterConformanceSuite` runs the database adapter contract case
15
23
  by case: value round-trips, ordering, guards, all-or-nothing writes, unique
16
24
  and multi-valued indexes, paging under capped native pages, and concurrent
17
25
  writers.
18
26
  - `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.
27
+ plugins costs at the adapter, on `createMeasuredDatabase`: `createHotUpdater`
28
+ over `meterReads({ name, adapter: verifyAdapter(adapter) })` from
29
+ `@hot-updater/plugin-core`. `postgresRowsExamined` and `mysqlRowsExamined`
30
+ also count the rows a SQL database examined.
22
31
  - `setupDatabaseTestSuite` runs core, Bundles and the Release Catalog contract
23
32
  through the admin and client HTTP APIs of `createHotUpdater`, then the suites
24
33
  of the server plugins listed in `plugins`, such as the Insights plugin's
25
- `insightsTestSuite()` from `@hot-updater/server/plugins/insights/testing`.
34
+ `insightsTestSuite()`.
26
35
 
27
36
  Connect your database's lifecycle and the real server:
28
37
 
29
38
  ```ts
30
39
  import { createHotUpdater } from "@hot-updater/server";
31
- import { createDatabasePluginApis } from "@hot-updater/server/db";
32
40
  import {
33
41
  createInsightsModel,
34
42
  insights,
35
43
  } from "@hot-updater/server/plugins/insights";
36
- import { insightsTestSuite } from "@hot-updater/server/plugins/insights/testing";
44
+
37
45
  import {
46
+ insightsTestSuite,
38
47
  setupDatabaseTestSuite,
39
48
  startHttpTestServer,
40
49
  } from "@hot-updater/test-utils";
@@ -63,9 +72,14 @@ setupDatabaseTestSuite({
63
72
  // The server above runs insights(), so its suite runs too.
64
73
  plugins: [
65
74
  insightsTestSuite({
75
+ // The plugin's API on the database, as a server running it assembles it.
66
76
  createModel: (database) =>
67
77
  createInsightsModel(
68
- createDatabasePluginApis(database, [insights()]).insights,
78
+ createHotUpdater({
79
+ database,
80
+ plugins: [insights()],
81
+ clientAccess: "public",
82
+ }).api.insights,
69
83
  ),
70
84
  }),
71
85
  ],
@@ -80,15 +94,84 @@ client selector and keep a simulated device's state across updates, rollbacks
80
94
  and the built-in fallback. Native download, activation and restart remain
81
95
  device end-to-end responsibilities.
82
96
 
97
+ ## Storage adapters
98
+
99
+ `setupStorageAdapterTestSuite` runs the storage adapter contract that deploy,
100
+ patch, the Console, the server and `storage prune` rely on: the canonical URI
101
+ of each key below the base path, streamed bodies, failed uploads that leave no
102
+ object, exact deletes, URIs of another bucket or protocol, download URLs, and
103
+ listing. A storage adapter's specs don't need `@hot-updater/server`.
104
+
105
+ ```ts
106
+ import { setupStorageAdapterTestSuite } from "@hot-updater/test-utils";
107
+
108
+ import { myStorage } from "./myStorage";
109
+
110
+ setupStorageAdapterTestSuite({
111
+ name: "myStorage",
112
+ createStorage: async () => ({
113
+ storage: myStorage({ bucket: "bundles", basePath: "ota" }),
114
+ basePath: "ota",
115
+ }),
116
+ operations: ["put", "get", "getDownloadUrl", "exists", "delete"],
117
+ });
118
+ ```
119
+
120
+ `createStorage` returns the adapter over an empty bucket or base path for each
121
+ case, and the `cleanup` it may return runs after the case. `operations` lists
122
+ what the adapter implements: the suite fails when one of them is missing and
123
+ skips the cases of the others.
124
+
125
+ With `fetchDownloadUrls: true`, the suite downloads every `http(s)` URL
126
+ `getDownloadUrl` returns and requires the object's bytes, for a spec against a
127
+ real bucket or emulator whose URLs the test can reach.
128
+
83
129
  ## Plugins and servers
84
130
 
85
- - `createPluginTestHarness` runs one server plugin the way `createHotUpdater`
86
- does, on a memory adapter by default, and measures what each API call reads.
131
+ - `createPluginTestHarness` runs one server plugin on `createEngine` from
132
+ `@hot-updater/plugin-core`, the engine `createHotUpdater` runs plugins on, on
133
+ a memory adapter by default, and measures what each API call reads.
87
134
  - `setupReleaseCatalogTestSuite` with `createHttpTestClient` tests a running
88
135
  server by URL. Use `createHandlerHttpTestClient` inside a Workers Vitest pool.
89
136
  - Server processes can import `createReleaseCatalogTestStorage` from
90
137
  `@hot-updater/test-utils/node` to install the suites' storage fixtures.
91
138
 
139
+ ## Client plugins
140
+
141
+ `@hot-updater/test-utils/react-native` runs client plugins the way the app
142
+ does. `setupClientPlugin(plugin, options)` and
143
+ `setupClientPlugins(plugins, options)` set plugins up as `HotUpdater.init`
144
+ does, on the plugin host from `@hot-updater/protocol`, the one the SDK runs,
145
+ against an in-memory device and a server the test answers. The entry loads
146
+ neither React Native nor Vitest, so plugin specs run in plain Node under
147
+ Vitest or Jest when the plugin imports `defineClientPlugin` from
148
+ `@hot-updater/protocol`; the SDK's root entry loads React Native.
149
+
150
+ ```ts
151
+ import { setupClientPlugin } from "@hot-updater/test-utils/react-native";
152
+
153
+ const runtime = setupClientPlugin(myPlugin(), {
154
+ requestHeaders: { "x-api-key": "client-key" },
155
+ });
156
+
157
+ runtime.hooks.onAppReady(launch);
158
+ await runtime.settled();
159
+
160
+ expect(runtime.requests[0]?.json()).toEqual(expectedEvent);
161
+ expect(runtime.errors).toEqual([]);
162
+ ```
163
+
164
+ `hooks` calls each hook as the SDK does, without waiting for it, and
165
+ `settled()` waits for what the hooks started. `requests` records what plugins
166
+ sent with `context.fetch`, and the `respond` option answers each request,
167
+ `204 No Content` by default. `errors` holds what the SDK reports to `onError`.
168
+ `storage` is the device's plugin storage: pass it to the next setup to start
169
+ the plugins again on the same device, or seed one with `createTestStorage()`.
170
+
92
171
  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.
172
+ [storage adapter guide](https://hot-updater.dev/docs/storage-adapters/custom-storage#test-the-adapter),
173
+ the
174
+ [database adapter guide](https://hot-updater.dev/docs/database-adapters/custom-database),
175
+ and the
176
+ [client plugin guide](https://hot-updater.dev/docs/react-native-api/client-plugins#test-the-plugin)
177
+ for the contracts and complete specs.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { ArtifactInfo, Bundle, ReleaseCatalog } from "@hot-updater/core";
2
- import { DatabaseAdapter, DatabaseReadCount, PhysicalTable } from "@hot-updater/plugin-core/internal";
3
- import { AggregateBatching, BundleRow, ChannelRow, DeployReleasePolicy, EngineDatabase, HotUpdaterCoreApi, ReleaseRow, StoragePlugin } from "@hot-updater/plugin-core";
1
+ import { ArtifactInfo, Bundle } from "@hot-updater/protocol";
2
+ import { AnyHotUpdaterPlugin, BundlePatchRow, BundleRow, ChannelRow, ClientAuth, DatabaseAdapter, DeployReleasePolicy, EngineDatabase, HotUpdaterCoreApi, HotUpdaterDatabase, KeyValueStore, ModuleSchema, PhysicalTable, PluginApis, ReadMeasurement, ReleaseRow, StorageAdapter, StorageOperation } from "@hot-updater/plugin-core";
3
+ import { BundleEventRow, InsightsModel } from "@hot-updater/plugin-insights/server";
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>;
@@ -128,7 +128,7 @@ type DatabaseTestSuiteOptions<TDatabase extends EngineDatabase = EngineDatabase>
128
128
  */
129
129
  readonly createHttpClient: (options: {
130
130
  readonly database: TDatabase;
131
- readonly storage: readonly StoragePlugin[];
131
+ readonly storage: readonly StorageAdapter[];
132
132
  }) => HttpTestServer | Promise<HttpTestServer>;
133
133
  /** The suites of the server plugins the provider opts in to test. */
134
134
  readonly plugins?: readonly DatabasePluginTestSuite<TDatabase>[];
@@ -175,9 +175,44 @@ declare const conformanceExpiring: PhysicalTable;
175
175
  */
176
176
  declare const setupDatabaseAdapterConformanceSuite: (options: DatabaseAdapterConformanceOptions) => void;
177
177
  //#endregion
178
+ //#region src/setupStorageAdapterTestSuite.d.ts
179
+ interface StorageAdapterTestSuiteOptions {
180
+ readonly name: string;
181
+ /**
182
+ * Returns the adapter over an empty bucket or base path. Every case creates
183
+ * its own and calls `cleanup` after it.
184
+ */
185
+ readonly createStorage: () => Promise<{
186
+ readonly storage: StorageAdapter;
187
+ /**
188
+ * The base path the adapter stores keys below. With it, every URI `put`
189
+ * returns must have the key `<basePath>/<key>`.
190
+ */
191
+ readonly basePath?: string;
192
+ readonly cleanup?: () => Promise<void>;
193
+ }>;
194
+ /**
195
+ * The operations the adapter implements. The suite requires them, with
196
+ * `put` and `get`, and skips the cases of every other operation. Without
197
+ * it, the suite skips the cases of the operations the adapter lacks.
198
+ */
199
+ readonly operations?: readonly StorageOperation[];
200
+ /**
201
+ * Fetches every `http(s)` URL `getDownloadUrl` returns and requires the
202
+ * object's bytes. Set it when the adapter runs against a bucket or emulator
203
+ * whose URLs the test can reach.
204
+ */
205
+ readonly fetchDownloadUrls?: boolean;
206
+ }
207
+ /**
208
+ * The storage adapter contract that deploy, patch, the Console, the server,
209
+ * and `storage prune` rely on. Every official storage adapter runs it.
210
+ */
211
+ declare const setupStorageAdapterTestSuite: (options: StorageAdapterTestSuiteOptions) => void;
212
+ //#endregion
178
213
  //#region src/sqlRowsExamined.d.ts
179
214
  /**
180
- * Rows a SQL database examined for the reads a storage adapter made: what
215
+ * Rows a SQL database examined for the reads a database adapter made: what
181
216
  * read-budget suites compare with the rows the adapter read.
182
217
  */
183
218
  interface RowsExamined {
@@ -237,72 +272,17 @@ declare const mysqlRowsExamined: (query: SqlQuery) => {
237
272
  };
238
273
  //#endregion
239
274
  //#region src/setupReadBudgetTestSuite.d.ts
240
- /** Core's API with the client routes' reads, which the public API leaves out. */
241
- type ReadBudgetCore = HotUpdaterCoreApi & {
242
- getReleaseCatalog(input: {
243
- readonly strategy: "APP_VERSION";
244
- readonly platform: "ios" | "android";
245
- readonly channelKey: string;
246
- readonly appVersion: string;
247
- }): Promise<ReleaseCatalog | null>;
248
- getArtifactInfo(targetBundleId: string, currentBundleId: string, artifactProtocolVersion: 1): Promise<ArtifactInfo | null>;
249
- latestReleaseId(scopeKey: string): Promise<string | null>;
250
- };
251
- /** How core reads bundle manifests and resolves file URLs. */
252
- interface ReadBudgetStorage {
253
- readonly readStorageText?: (storageUri: string) => Promise<string | null>;
254
- readonly resolveFileUrl: (storageUri: string | null) => Promise<string | null>;
255
- }
256
- /** What `createMeasuredDatabase` returns: core and the plugins on one engine in verify mode. */
257
- interface ReadBudgetDatabase {
258
- readonly core: ReadBudgetCore;
259
- readonly api: Readonly<Record<string, unknown>>;
260
- readonly clientAuth?: {
261
- authenticate(headers: Headers): Promise<boolean>;
262
- };
263
- measureReads<T>(read: () => Promise<T>): Promise<{
264
- readonly result: T;
265
- readonly adapter: DatabaseReadCount;
266
- readonly engine: {
267
- readonly calls: number;
268
- readonly rows: number;
269
- };
270
- }>;
271
- }
272
- /**
273
- * What the suite takes from `@hot-updater/server`; pass those exports, which
274
- * keeps test-utils free of a dependency on the server.
275
- */
276
- interface ReadBudgetServer {
277
- /** `createMeasuredDatabase` from `@hot-updater/server/db`. */
278
- createMeasuredDatabase(adapter: DatabaseAdapter, plugins: readonly unknown[], options: {
279
- readonly now: () => number;
280
- readonly storage: ReadBudgetStorage;
281
- }): ReadBudgetDatabase;
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
- };
288
- /** `[insights(), apiKeys()]`, the plugins whose reads have budgets. */
289
- readonly plugins: readonly unknown[];
290
- /** `targetBaseCandidateKey` from `@hot-updater/server/db`. */
291
- targetBaseCandidateKey(target: {
292
- readonly channel: string;
293
- readonly platform: "ios" | "android";
294
- readonly fingerprintHash: string | null;
295
- readonly appVersion: string | null;
296
- }): string | null;
297
- }
298
275
  interface ReadBudgetAdapterContext {
276
+ /**
277
+ * Core's tables, those of the plugins the suite measures, and the settings
278
+ * table, where the retention passes keep their lease.
279
+ */
299
280
  readonly tables: readonly PhysicalTable[];
300
281
  /** Test-only: the adapter must fetch at most this many rows per native page. */
301
282
  readonly nativePageSize: number;
302
283
  }
303
284
  interface ReadBudgetSuiteOptions {
304
285
  readonly name: string;
305
- readonly server: ReadBudgetServer;
306
286
  /** Returns an adapter over freshly created, empty tables. */
307
287
  readonly createAdapter: (context: ReadBudgetAdapterContext) => Promise<{
308
288
  readonly adapter: DatabaseAdapter;
@@ -317,44 +297,18 @@ interface ReadBudgetSuiteOptions {
317
297
  }>;
318
298
  }
319
299
  /**
320
- * The read-budget suite (PRD S4) on one backend: core and the built-in
321
- * plugins in verify mode over the backend's adapter, native pages capped at
322
- * two rows. Every API in the read-budget list reads exactly its budget at the
323
- * adapter and at the engine; with `examined`, the database examines no more
324
- * rows than the adapter read, within the backend's native multipliers.
300
+ * The read-budget suite (PRD S4) on one backend: core and the server's
301
+ * built-in plugins on `createMeasuredDatabase` over the backend's adapter,
302
+ * native pages capped at two rows. Every API in the read-budget list reads
303
+ * exactly its budget at the adapter and at the engine; with `examined`, the
304
+ * database examines no more rows than the adapter read, within the backend's
305
+ * native multipliers. It loads `@hot-updater/server`, an optional peer.
325
306
  */
326
307
  declare const setupReadBudgetTestSuite: (options: ReadBudgetSuiteOptions) => void;
327
308
  //#endregion
328
309
  //#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
310
  interface AggregateBatchingSuiteOptions {
355
311
  readonly name: string;
356
- /** `import * as engine from "@hot-updater/server/database"`. */
357
- readonly engine: AggregateBatchingEngine;
358
312
  /** Returns an adapter over an empty store; each case calls it once. */
359
313
  readonly createAdapter: () => Promise<{
360
314
  readonly adapter: DatabaseAdapter;
@@ -368,7 +322,7 @@ interface AggregateBatchingSuiteOptions {
368
322
  * writes with each transaction and a compaction merges once, and a memory
369
323
  * buffer, read back exactly as transactional writes read.
370
324
  */
371
- declare const setupAggregateBatchingTestSuite: ({ name, engine, createAdapter, oversizedRows }: AggregateBatchingSuiteOptions) => void;
325
+ declare const setupAggregateBatchingTestSuite: ({ name, createAdapter, oversizedRows }: AggregateBatchingSuiteOptions) => void;
372
326
  //#endregion
373
327
  //#region src/runContentionHarness.d.ts
374
328
  /** Delays every adapter call by `ms`, like a round trip to a remote database. */
@@ -396,40 +350,48 @@ interface ContentionReport {
396
350
  */
397
351
  declare const runContentionHarness: (options: ContentionOptions) => Promise<ContentionReport>;
398
352
  //#endregion
399
- //#region src/createPluginTestHarness.d.ts
400
- interface HarnessModule {
401
- readonly id: string;
402
- readonly schema: object;
403
- readonly namespace?: string;
353
+ //#region src/createMeasuredDatabase.d.ts
354
+ /** How core reads bundle manifests and resolves the file URLs of `storage://` URIs. */
355
+ interface MeasuredDatabaseStorage {
356
+ readonly readStorageText?: (storageUri: string) => Promise<string | null>;
357
+ readonly resolveFileUrl: (storageUri: string) => Promise<string>;
404
358
  }
405
- interface HarnessEngine {
406
- database(module: HarnessModule): unknown;
407
- measureReads<T>(read: () => Promise<T>): Promise<{
408
- readonly result: T;
409
- readonly adapter: DatabaseReadCount;
410
- readonly engine: {
411
- readonly calls: number;
412
- readonly rows: number;
413
- };
414
- }>;
359
+ interface MeasuredDatabaseOptions {
360
+ /**
361
+ * The plugins' clock, which they read through `ctx.now`. Core's
362
+ * timestamps and the retention passes run on `Date.now`.
363
+ */
364
+ readonly now?: () => number;
365
+ /**
366
+ * The storage of `storage://` URIs, for reads that resolve them, such as
367
+ * artifact resolution; without it those reads fail.
368
+ */
369
+ readonly storage?: MeasuredDatabaseStorage;
370
+ }
371
+ /** Core and the plugins' APIs on `createHotUpdater`, with the database's read meter. */
372
+ interface MeasuredDatabase<TApi = Readonly<Record<string, unknown>>> {
373
+ readonly core: HotUpdaterCoreApi;
374
+ readonly api: TApi;
375
+ /** The client-route policy, when a plugin provides one. */
376
+ readonly clientAuth?: ClientAuth;
377
+ /** Runs `read` and reports what it read at the adapter and at the engine. */
378
+ measureReads<T>(read: () => Promise<T>): Promise<ReadMeasurement<T>>;
415
379
  }
416
380
  /**
417
- * What the harness takes from `@hot-updater/server/database`; pass that
418
- * module, which keeps test-utils free of a dependency on the server.
381
+ * Core and the plugins over `adapter` as `createHotUpdater` runs them: what
382
+ * read-budget suites measure. The database is `meterReads` over
383
+ * `verifyAdapter(adapter)`, so every adapter call is checked against the
384
+ * contract and `measureReads` reports each call's reads; the adapter runs
385
+ * as given, without the schema fence. It loads `@hot-updater/server`, an
386
+ * optional peer, when called.
419
387
  */
420
- interface PluginTestEngine {
421
- resolveSchema(modules: readonly HarnessModule[]): {
422
- readonly tables: readonly PhysicalTable[];
423
- };
424
- createDatabaseEngine(options: {
425
- readonly adapter: DatabaseAdapter;
426
- readonly schema: never;
427
- readonly verify?: boolean;
428
- }): HarnessEngine;
429
- }
388
+ declare const createMeasuredDatabase: <const TPlugins extends readonly AnyHotUpdaterPlugin[]>(adapter: DatabaseAdapter, plugins: TPlugins, options?: MeasuredDatabaseOptions) => Promise<MeasuredDatabase<PluginApis<TPlugins>>>;
389
+ //#endregion
390
+ //#region src/createPluginTestHarness.d.ts
430
391
  interface HarnessPlugin {
431
392
  readonly id: string;
432
- readonly schema: object;
393
+ readonly schemaVersion: string;
394
+ readonly schema: ModuleSchema;
433
395
  /** `false` keeps the declared table names, as `createHotUpdater` does. */
434
396
  readonly namespace?: false;
435
397
  init(context: never): {
@@ -437,36 +399,111 @@ interface HarnessPlugin {
437
399
  };
438
400
  }
439
401
  interface PluginTestHarnessOptions {
440
- /** `import * as engine from "@hot-updater/server/database"`. */
441
- readonly engine: PluginTestEngine;
442
402
  /** Defaults to a fresh memory adapter. */
443
403
  readonly adapter?: DatabaseAdapter;
444
404
  /** The plugin's clock; `setNow` changes it later. */
445
405
  readonly now?: () => number;
446
406
  }
447
407
  /**
448
- * Runs one plugin the way `createHotUpdater` does, on a memory adapter in
449
- * verify mode, so tests can call its API and measure what each call reads.
408
+ * Runs one plugin on `createEngine`, the storage engine `createHotUpdater`
409
+ * runs its plugins on, so tests can call its API and measure what each call
410
+ * reads. Every adapter call is checked against the adapter contract
411
+ * (`verifyAdapter`), and `ctx.core` is empty: core's reads need a server.
412
+ * The engine gets the adapter without `prune`, as a store that expires rows
413
+ * itself would give it, so no retention pass adds to what a call reads and
414
+ * writes; rows stay past their retention.
450
415
  */
451
- declare const createPluginTestHarness: <TPlugin extends HarnessPlugin>(plugin: TPlugin, options: PluginTestHarnessOptions) => Promise<{
416
+ declare const createPluginTestHarness: <TPlugin extends HarnessPlugin>(plugin: TPlugin, options?: PluginTestHarnessOptions) => Promise<{
452
417
  api: ReturnType<TPlugin["init"]>["api"];
453
418
  instance: ReturnType<TPlugin["init"]>;
454
- /** The plugin's own database handle; cast it to its `HotUpdaterDatabase` type. */
455
- db: unknown;
419
+ /** The plugin's own database handle. */
420
+ db: HotUpdaterDatabase<TPlugin["schema"]>;
456
421
  adapter: DatabaseAdapter;
457
- tables: readonly PhysicalTable[];
458
- /** Reads at both boundaries while `read` runs. */
459
- measureReads: <T>(read: () => Promise<T>) => Promise<{
460
- readonly result: T;
461
- readonly adapter: DatabaseReadCount;
462
- readonly engine: {
463
- readonly calls: number;
464
- readonly rows: number;
465
- };
466
- }>;
467
- setNow: (now: () => number) => void;
422
+ /** The plugin's physical tables. */
423
+ tables: import("@hot-updater/plugin-core").PhysicalTable[];
424
+ /** Reads at the adapter and at the engine while `read` runs. */
425
+ measureReads: <T>(read: () => Promise<T>) => Promise<import("@hot-updater/plugin-core").ReadMeasurement<T>>;
426
+ setNow: (next: () => number) => void;
427
+ }>;
428
+ //#endregion
429
+ //#region src/databaseTestFixtures.d.ts
430
+ declare const createChannelRowFixture: (name?: string) => ChannelRow;
431
+ declare const createBundleRowFixture: (suffix: string, _channel?: string) => BundleRow;
432
+ declare const createBundlePatchRowFixture: (suffix: string, bundleId: string, baseBundleId: string, orderIndex?: number) => BundlePatchRow;
433
+ declare const createReleaseRowFixture: (suffix: string, bundle: BundleRow, channel: ChannelRow) => ReleaseRow;
434
+ declare const createBundleFixture: (suffix: string, _channel?: string) => Bundle;
435
+ //#endregion
436
+ //#region src/storeBundles.d.ts
437
+ /**
438
+ * Stores bundles and their patches with no release, as a database holds an
439
+ * artifact that no release uses. `deploy` always adds a release, and
440
+ * deleting a bundle's last release deletes the bundle, so tests store such
441
+ * bundles through the engine. A patch's base comes before it in `bundles`.
442
+ */
443
+ declare const storeBundles: (database: EngineDatabase, bundles: readonly Bundle[]) => Promise<void>;
444
+ //#endregion
445
+ //#region src/insights/expectInsightsIndex.d.ts
446
+ /** Allow committed secondary indexes to catch up; query failures still fail. */
447
+ declare const expectInsightsIndex: <T>(query: () => Promise<T>, expected: T) => Promise<void>;
448
+ //#endregion
449
+ //#region src/insights/fixtures.d.ts
450
+ /** An applied update from install `suffix`, received at `receivedAtMs`. */
451
+ declare const createBundleEventRowFixture: (suffix: string, receivedAtMs: number) => Extract<BundleEventRow, {
452
+ type: "UPDATE_APPLIED" | "RECOVERED";
468
453
  }>;
469
454
  //#endregion
455
+ //#region src/insights/setupInsightsHttpTestSuite.d.ts
456
+ /**
457
+ * The Insights plugin's routes on a database: a client reports an event, and
458
+ * the admin routes read it back. The server must run `insights()`.
459
+ */
460
+ declare const setupInsightsHttpTestSuite: (options: {
461
+ readonly getClient: () => HttpTestClient;
462
+ }) => void;
463
+ //#endregion
464
+ //#region src/insights/setupInsightsModelTestSuite.d.ts
465
+ /**
466
+ * The Insights report contract on one database: pass the Insights plugin's
467
+ * model over it, `createInsightsModel(createHotUpdater({ database, plugins:
468
+ * [insights()], clientAccess: "public" }).api.insights)`, as `createDatabase`.
469
+ */
470
+ declare const setupInsightsModelTestSuite: (lifecycle: DatabaseTestLifecycle<InsightsModel>) => void;
471
+ //#endregion
472
+ //#region src/insights/index.d.ts
473
+ /**
474
+ * The Insights plugin's tests, for `setupDatabaseTestSuite`'s `plugins`: its
475
+ * HTTP routes on the provider's server, which must run `insights()`, and
476
+ * with `createModel` its report contract on the provider's database, such as
477
+ * `(database) => createInsightsModel(createHotUpdater({ database, plugins:
478
+ * [insights()], clientAccess: "public" }).api.insights)`.
479
+ */
480
+ declare const insightsTestSuite: <TDatabase = EngineDatabase>(options?: {
481
+ readonly createModel?: (database: TDatabase) => InsightsModel;
482
+ }) => {
483
+ name: string;
484
+ register: (context: {
485
+ readonly getClient: () => HttpTestClient;
486
+ readonly getDatabase: () => TDatabase;
487
+ }) => void;
488
+ };
489
+ //#endregion
490
+ //#region src/kvTestStore.d.ts
491
+ interface MemoryKeyValueStoreOptions {
492
+ /** Test-only: the most items one `query` page returns. */
493
+ readonly nativePageSize?: number;
494
+ readonly limits?: KeyValueStore["limits"];
495
+ }
496
+ /**
497
+ * An in-memory key-value store that behaves like DynamoDB where the adapter
498
+ * can tell: a write checks every condition against the state before it,
499
+ * refuses two ops on one item, and applies all ops or none.
500
+ */
501
+ declare const createMemoryKeyValueStore: (options?: MemoryKeyValueStoreOptions) => KeyValueStore & {
502
+ readonly items: () => number;
503
+ /** Each item's expiry, as a store's native TTL would hold it, by `[pk, sk]` JSON. */
504
+ readonly expiries: () => ReadonlyMap<string, number>;
505
+ };
506
+ //#endregion
470
507
  //#region src/setupReleaseCatalogTestSuite.d.ts
471
508
  /**
472
509
  * Server conformance through HTTP only, on admin API protocol 2. The caller
@@ -494,6 +531,6 @@ declare const releaseCatalogArtifact: (bundle: BundleRow) => ArtifactInfo;
494
531
  /** Install these storage objects in the test server, independently of its DB. */
495
532
  declare const RELEASE_CATALOG_STORAGE_FIXTURES: Readonly<Record<string, string>>;
496
533
  /** Storage boundary shared by provider and separately spawned server tests. */
497
- declare const createReleaseCatalogTestStorage: () => StoragePlugin;
534
+ declare const createReleaseCatalogTestStorage: () => StorageAdapter;
498
535
  //#endregion
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 };
536
+ export { AdminApiTestClient, AggregateBatchingSuiteOptions, ContentionOptions, ContentionReport, DatabaseAdapterConformanceContext, DatabaseAdapterConformanceOptions, DatabasePluginTestSuite, type DatabaseTestLifecycle, DatabaseTestSuiteOptions, HttpTestClient, HttpTestHandlers, HttpTestRequest, HttpTestRequestInit, HttpTestServer, MeasuredDatabase, MeasuredDatabaseOptions, MeasuredDatabaseStorage, MemoryKeyValueStoreOptions, PluginTestHarnessOptions, RELEASE_CATALOG_DOWNLOAD_HASH, RELEASE_CATALOG_MANIFEST_URIS, RELEASE_CATALOG_STORAGE_FIXTURES, ReadBudgetAdapterContext, ReadBudgetSuiteOptions, RowsExamined, SqlExecutorLike, SqlQuery, SqlStatementLike, StorageAdapterTestSuiteOptions, conformanceCounters, conformanceExpiring, conformanceItems, createAdminApiTestClient, createBundleEventRowFixture, createBundleFixture, createBundlePatchRowFixture, createBundleRowFixture, createChannelRowFixture, createHandlerHttpTestClient, createHttpTestClient, createMeasuredDatabase, createMemoryKeyValueStore, createPluginTestHarness, createReleaseCatalogTestStorage, createReleaseRowFixture, expectInsightsIndex, insightsTestSuite, jsonRequest, mysqlRowsExamined, postgresRowsExamined, releaseCatalogArtifact, releaseCatalogDownloadUrl, runContentionHarness, setupAggregateBatchingTestSuite, setupBundleMethodsTestSuite, setupCoreAdminTestSuite, setupDatabaseAdapterConformanceSuite, setupDatabaseTestSuite, setupInsightsHttpTestSuite, setupInsightsModelTestSuite, setupReadBudgetTestSuite, setupReleaseCatalogTestSuite, setupSemverSatisfiesTestSuite, setupStorageAdapterTestSuite, startHttpTestServer, storeBundles, toDeployBundle, withAdapterLatency };