@hot-updater/test-utils 1.0.0-rc.15
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/LICENSE +33 -0
- package/README.md +64 -0
- package/dist/index.d.mts +452 -0
- package/dist/index.mjs +3569 -0
- package/dist/node.cjs +20442 -0
- package/dist/node.d.cts +107 -0
- package/dist/node.d.mts +107 -0
- package/dist/node.mjs +20392 -0
- package/package.json +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Sungyu Kang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
ADDITIONAL DISCLAIMER
|
|
26
|
+
|
|
27
|
+
The authors of this software shall not be held responsible for any legal issues,
|
|
28
|
+
regulatory violations, policy violations, or disputes arising from the use,
|
|
29
|
+
distribution, or integration of this software.
|
|
30
|
+
|
|
31
|
+
Users are solely responsible for ensuring compliance with all applicable laws,
|
|
32
|
+
regulations, and third-party policies (including but not limited to app store
|
|
33
|
+
policies).
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @hot-updater/test-utils
|
|
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.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pnpm add -D @hot-updater/test-utils @hot-updater/server vitest
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Connect your database lifecycle and the real server:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createHotUpdater } from "@hot-updater/server";
|
|
14
|
+
import {
|
|
15
|
+
setupDatabasePluginTestSuite,
|
|
16
|
+
startHttpTestServer,
|
|
17
|
+
} from "@hot-updater/test-utils";
|
|
18
|
+
import { myDatabase } from "./myDatabase";
|
|
19
|
+
import { db, migrateDatabase, resetDatabase } from "./testDatabase";
|
|
20
|
+
|
|
21
|
+
setupDatabasePluginTestSuite({
|
|
22
|
+
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
|
+
),
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
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.
|
|
50
|
+
|
|
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.
|
|
57
|
+
|
|
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.
|
|
62
|
+
|
|
63
|
+
See the [custom database guide](https://hot-updater.dev/docs/database-plugins/custom-database)
|
|
64
|
+
for the full setup and contract boundaries.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { ArtifactInfo, Bundle, ReleaseCatalog } from "@hot-updater/core";
|
|
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";
|
|
4
|
+
//#region src/httpTestClient.d.ts
|
|
5
|
+
type HttpTestRequestInit = NonNullable<ConstructorParameters<typeof Request>[1]>;
|
|
6
|
+
type HttpTestRequest = (path: string, init?: HttpTestRequestInit) => Promise<Response>;
|
|
7
|
+
interface HttpTestClient {
|
|
8
|
+
readonly client: HttpTestRequest;
|
|
9
|
+
readonly admin: HttpTestRequest;
|
|
10
|
+
}
|
|
11
|
+
interface HttpTestServer extends HttpTestClient {
|
|
12
|
+
readonly close: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
type HttpTestHandlers = Record<"client" | "admin", (request: Request) => Promise<Response>>;
|
|
15
|
+
/** Connect the same contract suite to any running HTTP server. */
|
|
16
|
+
declare function createHttpTestClient(options: {
|
|
17
|
+
readonly clientBaseUrl: string;
|
|
18
|
+
readonly adminBaseUrl: string;
|
|
19
|
+
readonly clientHeaders?: Record<string, string>;
|
|
20
|
+
readonly adminHeaders?: Record<string, string>;
|
|
21
|
+
}): HttpTestClient;
|
|
22
|
+
/** Workers run the same HTTP contract through their Web Request/Response boundary. */
|
|
23
|
+
declare function createHandlerHttpTestClient(handlers: HttpTestHandlers): HttpTestServer;
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/adminApiTestClient.d.ts
|
|
26
|
+
declare const jsonRequest: (method: string, body?: unknown) => HttpTestRequestInit;
|
|
27
|
+
/** A bundle row as the deploy route takes it. */
|
|
28
|
+
declare const toDeployBundle: (row: BundleRow) => Bundle;
|
|
29
|
+
/**
|
|
30
|
+
* Admin API protocol 2 over a test client. Each JSON call checks the answer
|
|
31
|
+
* is private and names the request when it fails. `track` records what a
|
|
32
|
+
* test wrote, and `cleanup` deletes it again: its releases, then its
|
|
33
|
+
* bundles, newest first.
|
|
34
|
+
*/
|
|
35
|
+
declare const createAdminApiTestClient: (getClient: () => HttpTestClient) => {
|
|
36
|
+
admin: (path: string, init?: HttpTestRequestInit) => Promise<Response>;
|
|
37
|
+
adminJson: (path: string, init?: HttpTestRequestInit) => Promise<any>;
|
|
38
|
+
deploy: (deployment: {
|
|
39
|
+
readonly bundle: Bundle;
|
|
40
|
+
readonly release: DeployReleasePolicy;
|
|
41
|
+
} | {
|
|
42
|
+
readonly bundleId: string;
|
|
43
|
+
readonly release: DeployReleasePolicy;
|
|
44
|
+
}) => Promise<ReleaseRow>;
|
|
45
|
+
ensureChannel: (name: string) => Promise<ChannelRow>;
|
|
46
|
+
removeRelease: (id: string) => Promise<void>;
|
|
47
|
+
/** Tracks a release the server wrote for a test, such as a promotion's copy. */
|
|
48
|
+
track: (releaseId: string) => void;
|
|
49
|
+
/** Stops tracking a release a test already deleted. */
|
|
50
|
+
forget: (releaseId: string) => void;
|
|
51
|
+
cleanup: () => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
type AdminApiTestClient = ReturnType<typeof createAdminApiTestClient>;
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/setupBundleMethodsTestSuite.d.ts
|
|
56
|
+
/**
|
|
57
|
+
* Bundles through admin API protocol 2: deployed with a release, read by
|
|
58
|
+
* id, paged by key, changed, and deleted. Each test deletes what it wrote.
|
|
59
|
+
*/
|
|
60
|
+
declare const setupBundleMethodsTestSuite: (options: {
|
|
61
|
+
readonly getClient: () => HttpTestClient;
|
|
62
|
+
}) => void;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/setupCoreAdminTestSuite.d.ts
|
|
65
|
+
/**
|
|
66
|
+
* Core's operations through admin API protocol 2 that the bundle and catalog
|
|
67
|
+
* suites leave out: channels, deploy refusals, promotion, preflights, and a
|
|
68
|
+
* bundle's patch children. Each test deletes what it wrote.
|
|
69
|
+
*/
|
|
70
|
+
declare const setupCoreAdminTestSuite: (options: {
|
|
71
|
+
readonly getClient: () => HttpTestClient;
|
|
72
|
+
}) => void;
|
|
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
|
+
//#region src/setupSemverSatisfiesTestSuite.d.ts
|
|
106
|
+
/**
|
|
107
|
+
*
|
|
108
|
+
* Filters based on semver. And sorts by the highest bundle version.
|
|
109
|
+
*
|
|
110
|
+
* * Range Expression Table:
|
|
111
|
+
*
|
|
112
|
+
* | Range Expression | Who gets the update |
|
|
113
|
+
* |------------------|------------------------------------------------------------------------|
|
|
114
|
+
* | 1.2.3 | Only devices running the specific binary app store version 1.2.3 of your app |
|
|
115
|
+
* | * | Any device configured to consume updates from your CodePush app |
|
|
116
|
+
* | 1.2.x | Devices running major version 1, minor version 2 and any patch version of your app |
|
|
117
|
+
* | 1.2.3 - 1.2.7 | Devices running any binary version between 1.2.3 (inclusive) and 1.2.7 (inclusive) |
|
|
118
|
+
* | >=1.2.3 <1.2.7 | Devices running any binary version between 1.2.3 (inclusive) and 1.2.7 (exclusive) |
|
|
119
|
+
* | 1.2 | Equivalent to >=1.2.0 <1.3.0 |
|
|
120
|
+
* | ~1.2.3 | Equivalent to >=1.2.3 <1.3.0 |
|
|
121
|
+
* | ^1.2.3 | Equivalent to >=1.2.3 <2.0.0 |
|
|
122
|
+
*/
|
|
123
|
+
declare const setupSemverSatisfiesTestSuite: ({ semverSatisfies }: {
|
|
124
|
+
semverSatisfies: (targetAppVersion: string, currentVersion: string) => Promise<boolean> | boolean;
|
|
125
|
+
}) => void;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/setupDatabaseTestSuite.d.ts
|
|
128
|
+
type DatabaseTestSuiteOptions<TDatabase extends EngineDatabase = EngineDatabase> = DatabaseTestLifecycle<TDatabase> & {
|
|
129
|
+
/**
|
|
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.
|
|
133
|
+
*/
|
|
134
|
+
readonly createHttpClient: (options: {
|
|
135
|
+
readonly database: TDatabase;
|
|
136
|
+
readonly storage: readonly StoragePlugin[];
|
|
137
|
+
}) => 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;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* 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.
|
|
150
|
+
*/
|
|
151
|
+
declare const setupDatabaseTestSuite: <TDatabase extends EngineDatabase>(options: DatabaseTestSuiteOptions<TDatabase>) => void;
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/setupDatabaseAdapterConformanceSuite.d.ts
|
|
154
|
+
interface DatabaseAdapterConformanceContext {
|
|
155
|
+
readonly tables: readonly PhysicalTable[];
|
|
156
|
+
/** Test-only: the adapter must fetch at most this many rows per native page. */
|
|
157
|
+
readonly nativePageSize?: number;
|
|
158
|
+
}
|
|
159
|
+
interface DatabaseAdapterConformanceOptions {
|
|
160
|
+
readonly name: string;
|
|
161
|
+
/** Returns an adapter over freshly created, empty tables. */
|
|
162
|
+
readonly createAdapter: (context: DatabaseAdapterConformanceContext) => Promise<{
|
|
163
|
+
readonly adapter: DatabaseAdapter;
|
|
164
|
+
readonly cleanup?: () => Promise<void>;
|
|
165
|
+
}>;
|
|
166
|
+
/** The most ops one atomic write accepts; omit when the backend has no limit. */
|
|
167
|
+
readonly maxOps?: number;
|
|
168
|
+
/** Concurrent writers in the contention cases (default 32). */
|
|
169
|
+
readonly writers?: number;
|
|
170
|
+
}
|
|
171
|
+
declare const conformanceItems: PhysicalTable;
|
|
172
|
+
declare const conformanceCounters: PhysicalTable;
|
|
173
|
+
/**
|
|
174
|
+
* The storage-adapter conformance contract (PRD S3 and S4 cases). Every
|
|
175
|
+
* adapter runs it through `verifyAdapter`, which also checks each read and
|
|
176
|
+
* write at the adapter boundary.
|
|
177
|
+
*/
|
|
178
|
+
declare const setupDatabaseAdapterConformanceSuite: (options: DatabaseAdapterConformanceOptions) => void;
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/sqlRowsExamined.d.ts
|
|
181
|
+
/**
|
|
182
|
+
* Rows a SQL database examined for the reads a storage adapter made: what
|
|
183
|
+
* read-budget suites compare with the rows the adapter read.
|
|
184
|
+
*/
|
|
185
|
+
interface RowsExamined {
|
|
186
|
+
/** Counts the reads from now on, and only those. */
|
|
187
|
+
reset(): void;
|
|
188
|
+
/** Rows examined for the reads since `reset`. */
|
|
189
|
+
total(): Promise<number>;
|
|
190
|
+
/** Rows the backend examines natively for each row it returns (default 1). */
|
|
191
|
+
readonly perRow?: number;
|
|
192
|
+
/** Rows the backend examines natively for each read beyond those (default 0). */
|
|
193
|
+
readonly perRead?: number;
|
|
194
|
+
}
|
|
195
|
+
/** A statement as the SQL core runs it. */
|
|
196
|
+
interface SqlStatementLike {
|
|
197
|
+
readonly sql: string;
|
|
198
|
+
readonly params: readonly unknown[];
|
|
199
|
+
}
|
|
200
|
+
/** A SQL core executor: its top-level `execute` runs the adapter's reads. */
|
|
201
|
+
interface SqlExecutorLike {
|
|
202
|
+
execute(statement: SqlStatementLike): Promise<unknown>;
|
|
203
|
+
}
|
|
204
|
+
/** Runs one statement on the session reads are explained on. */
|
|
205
|
+
type SqlQuery = (sql: string, params: readonly unknown[]) => Promise<readonly Record<string, unknown>[]>;
|
|
206
|
+
/**
|
|
207
|
+
* Rows examined on PostgreSQL (PGlite or a server). Each SELECT a wrapped
|
|
208
|
+
* executor runs is first run under `EXPLAIN (ANALYZE, FORMAT JSON)` on
|
|
209
|
+
* `query`, one session with the planner settings above; each `reset` first
|
|
210
|
+
* vacuums and analyzes, as autovacuum keeps a production database. A plan
|
|
211
|
+
* then takes the index path a production-sized table takes, and a read no
|
|
212
|
+
* index serves still scans. A statement counts the rows its busiest table
|
|
213
|
+
* read examined, so an exact plan counts the rows it returns, a multi-valued
|
|
214
|
+
* index's own table included.
|
|
215
|
+
*/
|
|
216
|
+
declare const postgresRowsExamined: (query: SqlQuery) => {
|
|
217
|
+
/** The executor, examining each SELECT it runs outside a transaction. */
|
|
218
|
+
wrap: <T extends SqlExecutorLike>(executor: T) => T;
|
|
219
|
+
examined: {
|
|
220
|
+
reset(): void;
|
|
221
|
+
total(): Promise<number>;
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Rows examined on MySQL. Each SELECT a wrapped executor runs is first run
|
|
226
|
+
* under `EXPLAIN ANALYZE` on `query`, one session, after `ANALYZE TABLE` of
|
|
227
|
+
* the tables it reads once per `reset`, and with multi-range reads always
|
|
228
|
+
* on, so a secondary index's range is costed with sorted row reads, as a
|
|
229
|
+
* production-sized table's is. A statement counts the rows its busiest table
|
|
230
|
+
* read examined; a filter above a table read drops rows that read produced.
|
|
231
|
+
*/
|
|
232
|
+
declare const mysqlRowsExamined: (query: SqlQuery) => {
|
|
233
|
+
/** The executor, examining each SELECT it runs outside a transaction. */
|
|
234
|
+
wrap: <T extends SqlExecutorLike>(executor: T) => T;
|
|
235
|
+
examined: {
|
|
236
|
+
reset(): void;
|
|
237
|
+
total(): Promise<number>;
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/setupReadBudgetTestSuite.d.ts
|
|
242
|
+
/** Core's API with the client routes' reads, which the public API leaves out. */
|
|
243
|
+
type ReadBudgetCore = HotUpdaterCoreApi & {
|
|
244
|
+
getReleaseCatalog(input: {
|
|
245
|
+
readonly strategy: "APP_VERSION";
|
|
246
|
+
readonly platform: "ios" | "android";
|
|
247
|
+
readonly channelKey: string;
|
|
248
|
+
readonly appVersion: string;
|
|
249
|
+
}): Promise<ReleaseCatalog | null>;
|
|
250
|
+
getArtifactInfo(targetBundleId: string, currentBundleId: string, artifactProtocolVersion: 1): Promise<ArtifactInfo | null>;
|
|
251
|
+
latestReleaseId(scopeKey: string): Promise<string | null>;
|
|
252
|
+
};
|
|
253
|
+
/** How core reads bundle manifests and resolves file URLs. */
|
|
254
|
+
interface ReadBudgetStorage {
|
|
255
|
+
readonly readStorageText?: (storageUri: string) => Promise<string | null>;
|
|
256
|
+
readonly resolveFileUrl: (storageUri: string | null) => Promise<string | null>;
|
|
257
|
+
}
|
|
258
|
+
/** What `createMeasuredDatabase` returns: core and the plugins on one engine in verify mode. */
|
|
259
|
+
interface ReadBudgetDatabase {
|
|
260
|
+
readonly core: ReadBudgetCore;
|
|
261
|
+
readonly api: Readonly<Record<string, unknown>>;
|
|
262
|
+
readonly clientAuth?: {
|
|
263
|
+
authenticate(headers: Headers): Promise<boolean>;
|
|
264
|
+
};
|
|
265
|
+
measureReads<T>(read: () => Promise<T>): Promise<{
|
|
266
|
+
readonly result: T;
|
|
267
|
+
readonly adapter: DatabaseReadCount;
|
|
268
|
+
readonly engine: {
|
|
269
|
+
readonly calls: number;
|
|
270
|
+
readonly rows: number;
|
|
271
|
+
};
|
|
272
|
+
}>;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* What the suite takes from `@hot-updater/server`; pass those exports, which
|
|
276
|
+
* keeps test-utils free of a dependency on the server.
|
|
277
|
+
*/
|
|
278
|
+
interface ReadBudgetServer {
|
|
279
|
+
/** `createMeasuredDatabase` from `@hot-updater/server/db`. */
|
|
280
|
+
createMeasuredDatabase(adapter: DatabaseAdapter, plugins: readonly unknown[], options: {
|
|
281
|
+
readonly now: () => number;
|
|
282
|
+
readonly storage: ReadBudgetStorage;
|
|
283
|
+
}): ReadBudgetDatabase;
|
|
284
|
+
/** `builtInSchema` from `@hot-updater/server/database`: the tables to create. */
|
|
285
|
+
readonly builtInSchema: {
|
|
286
|
+
readonly tables: readonly PhysicalTable[];
|
|
287
|
+
};
|
|
288
|
+
/** `[insights(), apiKeys()]`, the built-in plugins whose reads have budgets. */
|
|
289
|
+
readonly plugins: readonly unknown[];
|
|
290
|
+
/** `targetBaseCandidateKey` from `@hot-updater/server/db`. */
|
|
291
|
+
targetBaseCandidateKey(target: {
|
|
292
|
+
readonly channelId: string;
|
|
293
|
+
readonly platform: string;
|
|
294
|
+
readonly fingerprintHash: string | null;
|
|
295
|
+
readonly appVersion: string | null;
|
|
296
|
+
}): string | null;
|
|
297
|
+
}
|
|
298
|
+
interface ReadBudgetAdapterContext {
|
|
299
|
+
readonly tables: readonly PhysicalTable[];
|
|
300
|
+
/** Test-only: the adapter must fetch at most this many rows per native page. */
|
|
301
|
+
readonly nativePageSize: number;
|
|
302
|
+
}
|
|
303
|
+
interface ReadBudgetSuiteOptions {
|
|
304
|
+
readonly name: string;
|
|
305
|
+
readonly server: ReadBudgetServer;
|
|
306
|
+
/** Returns an adapter over freshly created, empty tables. */
|
|
307
|
+
readonly createAdapter: (context: ReadBudgetAdapterContext) => Promise<{
|
|
308
|
+
readonly adapter: DatabaseAdapter;
|
|
309
|
+
readonly cleanup?: () => Promise<void>;
|
|
310
|
+
/** SQL backends: the rows the database examined for the adapter's reads. */
|
|
311
|
+
readonly examined?: RowsExamined;
|
|
312
|
+
/**
|
|
313
|
+
* Index reads return row copies without `_v`, as the key-value adapter's
|
|
314
|
+
* do, so a transaction reads the rows it guards again, whole.
|
|
315
|
+
*/
|
|
316
|
+
readonly indexCopies?: boolean;
|
|
317
|
+
}>;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
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.
|
|
325
|
+
*/
|
|
326
|
+
declare const setupReadBudgetTestSuite: (options: ReadBudgetSuiteOptions) => void;
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/runContentionHarness.d.ts
|
|
329
|
+
/** Delays every adapter call by `ms`, like a round trip to a remote database. */
|
|
330
|
+
declare const withAdapterLatency: (adapter: DatabaseAdapter, ms: number) => DatabaseAdapter;
|
|
331
|
+
interface ContentionOptions {
|
|
332
|
+
/** How many transactions to start. */
|
|
333
|
+
readonly transactions: number;
|
|
334
|
+
/** Arrivals per second, spaced evenly. */
|
|
335
|
+
readonly ratePerSecond: number;
|
|
336
|
+
/** At most this many run at once, like a connection pool. */
|
|
337
|
+
readonly concurrency: number;
|
|
338
|
+
/** Runs transaction `index` to completion. */
|
|
339
|
+
readonly run: (index: number) => Promise<unknown>;
|
|
340
|
+
}
|
|
341
|
+
interface ContentionReport {
|
|
342
|
+
readonly transactions: number;
|
|
343
|
+
readonly committed: number;
|
|
344
|
+
/** Errors thrown by `run`, counted by error name. */
|
|
345
|
+
readonly errors: Readonly<Record<string, number>>;
|
|
346
|
+
readonly durationMs: number;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Starts `transactions` runs at a steady arrival rate through a fixed pool and
|
|
350
|
+
* reports how they ended. Retry counts come from the engine's `onRetry`.
|
|
351
|
+
*/
|
|
352
|
+
declare const runContentionHarness: (options: ContentionOptions) => Promise<ContentionReport>;
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/createPluginTestHarness.d.ts
|
|
355
|
+
interface HarnessModule {
|
|
356
|
+
readonly id: string;
|
|
357
|
+
readonly schema: object;
|
|
358
|
+
readonly namespace?: string;
|
|
359
|
+
}
|
|
360
|
+
interface HarnessEngine {
|
|
361
|
+
database(module: HarnessModule): unknown;
|
|
362
|
+
measureReads<T>(read: () => Promise<T>): Promise<{
|
|
363
|
+
readonly result: T;
|
|
364
|
+
readonly adapter: DatabaseReadCount;
|
|
365
|
+
readonly engine: {
|
|
366
|
+
readonly calls: number;
|
|
367
|
+
readonly rows: number;
|
|
368
|
+
};
|
|
369
|
+
}>;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* What the harness takes from `@hot-updater/server/database`; pass that
|
|
373
|
+
* module, which keeps test-utils free of a dependency on the server.
|
|
374
|
+
*/
|
|
375
|
+
interface PluginTestEngine {
|
|
376
|
+
resolveSchema(modules: readonly HarnessModule[]): {
|
|
377
|
+
readonly tables: readonly PhysicalTable[];
|
|
378
|
+
};
|
|
379
|
+
createDatabaseEngine(options: {
|
|
380
|
+
readonly adapter: DatabaseAdapter;
|
|
381
|
+
readonly schema: never;
|
|
382
|
+
readonly verify?: boolean;
|
|
383
|
+
}): HarnessEngine;
|
|
384
|
+
}
|
|
385
|
+
interface HarnessPlugin {
|
|
386
|
+
readonly id: string;
|
|
387
|
+
readonly schema: object;
|
|
388
|
+
init(context: never): {
|
|
389
|
+
readonly api: unknown;
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
interface PluginTestHarnessOptions {
|
|
393
|
+
/** `import * as engine from "@hot-updater/server/database"`. */
|
|
394
|
+
readonly engine: PluginTestEngine;
|
|
395
|
+
/** Defaults to a fresh memory adapter. */
|
|
396
|
+
readonly adapter?: DatabaseAdapter;
|
|
397
|
+
/** The plugin's clock; `setNow` changes it later. */
|
|
398
|
+
readonly now?: () => number;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Runs one plugin the way `createHotUpdater` does, on a memory adapter in
|
|
402
|
+
* verify mode, so tests can call its API and measure what each call reads.
|
|
403
|
+
*/
|
|
404
|
+
declare const createPluginTestHarness: <TPlugin extends HarnessPlugin>(plugin: TPlugin, options: PluginTestHarnessOptions) => Promise<{
|
|
405
|
+
api: ReturnType<TPlugin["init"]>["api"];
|
|
406
|
+
instance: ReturnType<TPlugin["init"]>;
|
|
407
|
+
/** The plugin's own database handle; cast it to its `HotUpdaterDatabase` type. */
|
|
408
|
+
db: unknown;
|
|
409
|
+
adapter: DatabaseAdapter;
|
|
410
|
+
tables: readonly PhysicalTable[];
|
|
411
|
+
/** Reads at both boundaries while `read` runs. */
|
|
412
|
+
measureReads: <T>(read: () => Promise<T>) => Promise<{
|
|
413
|
+
readonly result: T;
|
|
414
|
+
readonly adapter: DatabaseReadCount;
|
|
415
|
+
readonly engine: {
|
|
416
|
+
readonly calls: number;
|
|
417
|
+
readonly rows: number;
|
|
418
|
+
};
|
|
419
|
+
}>;
|
|
420
|
+
setNow: (now: () => number) => void;
|
|
421
|
+
}>;
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/setupReleaseCatalogTestSuite.d.ts
|
|
424
|
+
/**
|
|
425
|
+
* Server conformance through HTTP only, on admin API protocol 2. The caller
|
|
426
|
+
* owns server, database, and storage setup. Releases are written with
|
|
427
|
+
* `POST /releases` and changed with `PATCH /releases/:id`; lifecycle scenarios
|
|
428
|
+
* feed HTTP catalogs to the production client selector. Each test deletes
|
|
429
|
+
* what it wrote, so a long-lived server can run the suite again.
|
|
430
|
+
*/
|
|
431
|
+
declare const setupReleaseCatalogTestSuite: (options: {
|
|
432
|
+
readonly getClient: () => HttpTestClient;
|
|
433
|
+
}) => void;
|
|
434
|
+
//#endregion
|
|
435
|
+
//#region src/httpTestServer.d.ts
|
|
436
|
+
/** Serve Web handlers over loopback TCP; contract tests use real fetch requests. */
|
|
437
|
+
declare function startHttpTestServer(handlers: HttpTestHandlers): Promise<HttpTestServer>;
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/releaseCatalogHttpFixtures.d.ts
|
|
440
|
+
declare const releaseCatalogDownloadUrl: (uri: string) => string;
|
|
441
|
+
declare const RELEASE_CATALOG_MANIFEST_URIS: {
|
|
442
|
+
small: string;
|
|
443
|
+
large: string;
|
|
444
|
+
};
|
|
445
|
+
declare const RELEASE_CATALOG_DOWNLOAD_HASH: string;
|
|
446
|
+
declare const releaseCatalogArtifact: (bundle: BundleRow) => ArtifactInfo;
|
|
447
|
+
/** Install these storage objects in the test server, independently of its DB. */
|
|
448
|
+
declare const RELEASE_CATALOG_STORAGE_FIXTURES: Readonly<Record<string, string>>;
|
|
449
|
+
/** Storage boundary shared by provider and separately spawned server tests. */
|
|
450
|
+
declare const createReleaseCatalogTestStorage: () => StoragePlugin;
|
|
451
|
+
//#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 };
|