@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 +65 -43
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +59 -10
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,64 +1,86 @@
|
|
|
1
1
|
# @hot-updater/test-utils
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
reset: () =>
|
|
26
|
-
dispose: async (
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
64
|
-
|
|
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
|
|
293
|
-
readonly platform:
|
|
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
|
-
|
|
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:
|
|
3028
|
+
api: "deploy base candidates: 1 catalog point read",
|
|
3010
3029
|
read: ({ core, candidateKey }) => core.findBaseBundleIds(candidateKey, createBundleFixture("107").id, 2),
|
|
3011
|
-
adapter: reads(
|
|
3030
|
+
adapter: reads(1, 1, 0, 0),
|
|
3012
3031
|
engine: {
|
|
3013
3032
|
calls: 1,
|
|
3014
|
-
rows:
|
|
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
|
|
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(
|
|
3079
|
+
adapter: reads(1, 7, 4, 6),
|
|
3046
3080
|
engine: {
|
|
3047
|
-
calls:
|
|
3048
|
-
rows:
|
|
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 ?
|
|
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,
|
|
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.
|
|
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.
|
|
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/
|
|
51
|
-
"@hot-updater/
|
|
52
|
-
"@hot-updater/
|
|
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",
|