velocious 1.0.667 → 1.0.668
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 +27 -7
- package/build/configuration.js +23 -1
- package/build/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
- package/build/environment-handlers/node/cli/commands/test.js +17 -69
- package/build/frontend-models/websocket-channel.js +18 -0
- package/build/http-server/client/websocket-session.js +41 -4
- package/build/http-server/websocket-channel.js +14 -0
- package/build/http-server/websocket-event-log-store.js +85 -12
- package/build/http-server/websocket-events-host.js +14 -6
- package/build/src/configuration.d.ts +18 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +22 -2
- package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +3 -47
- package/build/src/environment-handlers/node/cli/commands/test.d.ts +2 -28
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +10 -61
- package/build/src/frontend-models/websocket-channel.d.ts +9 -0
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +16 -1
- package/build/src/http-server/client/websocket-session.d.ts +25 -2
- package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
- package/build/src/http-server/client/websocket-session.js +40 -5
- package/build/src/http-server/websocket-channel.d.ts +11 -0
- package/build/src/http-server/websocket-channel.d.ts.map +1 -1
- package/build/src/http-server/websocket-channel.js +14 -1
- package/build/src/http-server/websocket-event-log-store.d.ts +60 -41
- package/build/src/http-server/websocket-event-log-store.d.ts.map +1 -1
- package/build/src/http-server/websocket-event-log-store.js +79 -12
- package/build/src/http-server/websocket-events-host.d.ts +10 -3
- package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
- package/build/src/http-server/websocket-events-host.js +14 -7
- package/build/src/testing/test-files-finder.d.ts +13 -116
- package/build/src/testing/test-files-finder.d.ts.map +1 -1
- package/build/src/testing/test-files-finder.js +26 -296
- package/build/src/testing/test-filter-parser.d.ts +32 -22
- package/build/src/testing/test-filter-parser.d.ts.map +1 -1
- package/build/src/testing/test-filter-parser.js +25 -217
- package/build/src/testing/test-profile-activity.d.ts +1 -6
- package/build/src/testing/test-profile-activity.d.ts.map +1 -1
- package/build/src/testing/test-profile-activity.js +2 -12
- package/build/src/testing/test-profile-output.d.ts +1 -40
- package/build/src/testing/test-profile-output.d.ts.map +1 -1
- package/build/src/testing/test-profile-output.js +2 -217
- package/build/src/testing/test-profiler.d.ts +15 -659
- package/build/src/testing/test-profiler.d.ts.map +1 -1
- package/build/src/testing/test-profiler.js +35 -833
- package/build/src/testing/test-runner.d.ts +10 -1
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +26 -4
- package/build/src/testing/test-suite-splitter.d.ts +1 -128
- package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
- package/build/src/testing/test-suite-splitter.js +2 -259
- package/build/src/testing/timing-manifest.d.ts +5 -93
- package/build/src/testing/timing-manifest.d.ts.map +1 -1
- package/build/src/testing/timing-manifest.js +4 -283
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
- package/build/src/testing/velocious-attempt-executor.js +8 -2
- package/build/testing/test-files-finder.js +29 -345
- package/build/testing/test-filter-parser.js +29 -248
- package/build/testing/test-profile-activity.js +1 -12
- package/build/testing/test-profile-output.js +8 -253
- package/build/testing/test-profiler.js +38 -895
- package/build/testing/test-runner.js +33 -9
- package/build/testing/test-suite-splitter.js +1 -301
- package/build/testing/timing-manifest.js +10 -344
- package/build/testing/velocious-attempt-executor.js +7 -1
- package/package.json +3 -3
- package/scripts/test-browser.js +29 -6
- package/scripts/verify-docker-dev-environment.js +13 -1
- package/src/configuration.js +23 -1
- package/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
- package/src/environment-handlers/node/cli/commands/test.js +17 -69
- package/src/frontend-models/websocket-channel.js +18 -0
- package/src/http-server/client/websocket-session.js +41 -4
- package/src/http-server/websocket-channel.js +14 -0
- package/src/http-server/websocket-event-log-store.js +85 -12
- package/src/http-server/websocket-events-host.js +14 -6
- package/src/testing/test-files-finder.js +29 -345
- package/src/testing/test-filter-parser.js +29 -248
- package/src/testing/test-profile-activity.js +1 -12
- package/src/testing/test-profile-output.js +8 -253
- package/src/testing/test-profiler.js +38 -895
- package/src/testing/test-runner.js +33 -9
- package/src/testing/test-suite-splitter.js +1 -301
- package/src/testing/timing-manifest.js +10 -344
- package/src/testing/velocious-attempt-executor.js +7 -1
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
|
|
33
33
|
* Translated model attributes with current-locale relationship sorting (see [docs/translations.md](docs/translations.md))
|
|
34
34
|
* Cross-process broadcast bus for `broadcastToChannel` via `velocious beacon`, including background job runner processes (see [docs/beacon.md](docs/beacon.md))
|
|
35
|
+
* Websocket channel primitives for 1:N pub/sub over the shared session socket (`channel-subscribe` protocol): `matches()`-routed streams, interest-gated event-log persistence with 10-minute retention, stream-scoped `lastEventId` replay with `channel-replay-gap` gap reporting, and `{liveOnly: true}` channels that never reach the replay log (see [docs/websocket-channels.md](docs/websocket-channels.md))
|
|
35
36
|
* Rails-style application process initializer teardown with immutable process identity, reverse idempotent shutdown, and explicit HTTP/background-job ownership (see [docs/application-process-lifecycle.md](docs/application-process-lifecycle.md))
|
|
36
37
|
* Configurable HTTP server worker handlers plus backpressured, descriptor-only file responses with completion callbacks (see [docs/http-server.md](docs/http-server.md))
|
|
37
38
|
* Explicit database-free in-process HTTP applications with optional request and buffered-response byte limits (see [docs/http-server.md](docs/http-server.md#database-free-applications))
|
|
@@ -125,7 +126,7 @@ Maintainers cutting a package release must follow the [Velocious release runbook
|
|
|
125
126
|
|
|
126
127
|
# Docker development environment
|
|
127
128
|
|
|
128
|
-
The checked-in root `Dockerfile` and `compose.yml` define one canonical `dev` service used by humans, CI, and agent systems alike (see [docs/docker-development-environment.md](docs/docker-development-environment.md)). The image is Ubuntu 26.04 LTS (pinned by digest) with Node.js 24.x from signed NodeSource, the universal apt coding/debugging baseline,
|
|
129
|
+
The checked-in root `Dockerfile` and `compose.yml` define one canonical `dev` service used by humans, CI, and agent systems alike (see [docs/docker-development-environment.md](docs/docker-development-environment.md)). The image is Ubuntu 26.04 LTS (pinned by digest) with Node.js 24.x from signed NodeSource, the universal apt coding/debugging baseline, the newest published retained provider CLIs, and owner-pinned native Qwen Code 0.23.3; it is source-independent — no project source is copied and no project dependencies are installed at image build time.
|
|
129
130
|
|
|
130
131
|
Prerequisites: Docker with the Compose v2 plugin, and this repository checked out at `$DEV_HOME_PATH/velocious` (default `DEV_HOME_PATH`: `/home/dev`).
|
|
131
132
|
|
|
@@ -179,10 +180,13 @@ Baselines are generated against a fresh checkout (no generated dummy `configurat
|
|
|
179
180
|
# Testing
|
|
180
181
|
|
|
181
182
|
Application tests may import the testing DSL from the independent public package.
|
|
182
|
-
`@velocious/testing` `0.0.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
`@velocious/testing` `0.0.14` owns framework-neutral discovery, filtering,
|
|
184
|
+
execution, reporting, deterministic weighted grouping, profiling, and timing
|
|
185
|
+
manifests. Compatible installed copies share one protocol-1/schema-3 default registry.
|
|
186
|
+
Velocious retains application/configuration startup, database and transaction
|
|
187
|
+
lifecycle, browser execution, environment handling, framework profile spans, and
|
|
188
|
+
report translation. Existing Velocious import paths remain supported as thin
|
|
189
|
+
delegating compatibility facades.
|
|
186
190
|
|
|
187
191
|
```js
|
|
188
192
|
import {describe, expect, it} from "@velocious/testing"
|
|
@@ -245,7 +249,9 @@ export default async function configureTesting() {
|
|
|
245
249
|
}
|
|
246
250
|
```
|
|
247
251
|
|
|
248
|
-
Retry flaky tests by setting a retry count on the test args
|
|
252
|
+
Retry flaky tests by setting a retry count on the test args, or set CLI defaults for
|
|
253
|
+
retries and lifecycle timeout. Import repeatable global setup before the testing
|
|
254
|
+
configuration and test declarations with `--setup`.
|
|
249
255
|
|
|
250
256
|
```js
|
|
251
257
|
describe("Tasks", () => {
|
|
@@ -253,6 +259,10 @@ it("retries a flaky check", {retry: 2}, async () => {})
|
|
|
253
259
|
})
|
|
254
260
|
```
|
|
255
261
|
|
|
262
|
+
```bash
|
|
263
|
+
npx velocious test --retry=2 --timeout=30000 --setup=spec/setup.js spec/
|
|
264
|
+
```
|
|
265
|
+
|
|
256
266
|
Velocious prints the slowest tests after every run so suite hotspots are easy to spot. Each line shows the duration, full description and `file:line`.
|
|
257
267
|
|
|
258
268
|
```bash
|
|
@@ -295,7 +305,7 @@ npx velocious test:timing-manifest:merge --output tmp/test-timings.json \
|
|
|
295
305
|
See [test profiling](docs/test-profiling.md) for lifecycle accounting, custom
|
|
296
306
|
activity spans, schema, and privacy guarantees.
|
|
297
307
|
|
|
298
|
-
Prefer waiting for a real signal or condition over sleeping a fixed duration. `waitForEvent(emitter, eventName, {timeoutMs, filter})` resolves the instant a matching event fires (a background job finishing, a model update, a websocket message) and rejects on timeout; for polling an arbitrary condition, use awaitery's `waitFor`. Both `@velocious/testing` and the backward-compatible `velocious/build/src/testing/test.js` facade are supported imports; the Velocious runner consumes public-package declarations and adds the framework-specific database, request,
|
|
308
|
+
Prefer waiting for a real signal or condition over sleeping a fixed duration. `waitForEvent(emitter, eventName, {timeoutMs, filter})` resolves the instant a matching event fires (a background job finishing, a model update, a websocket message) and rejects on timeout; for polling an arbitrary condition, use awaitery's `waitFor`. Both `@velocious/testing` and the backward-compatible `velocious/build/src/testing/test.js` facade are supported imports; the Velocious runner consumes public-package declarations and adds the framework-specific startup, database, request, browser, profile-span, and cleanup behavior.
|
|
299
309
|
|
|
300
310
|
```js
|
|
301
311
|
import {waitForEvent} from "velocious/build/src/testing/test.js"
|
|
@@ -2224,6 +2234,16 @@ socket.send(JSON.stringify({
|
|
|
2224
2234
|
}))
|
|
2225
2235
|
```
|
|
2226
2236
|
|
|
2237
|
+
### V2 channel registration, replay, and live-only channels
|
|
2238
|
+
|
|
2239
|
+
`configuration.registerWebsocketChannel(name, ChannelClass)` registers a channel class for the `channel-subscribe` protocol — distinct from the `websocketChannelResolver` above, which serves the legacy `{type: "subscribe"}` path. Subscribers are routed by the class's `matches(broadcastParams)`, and publishers emit with `configuration.broadcastToChannel(name, broadcastParams, body)`.
|
|
2240
|
+
|
|
2241
|
+
- **Stream-scoped replay.** Delivered `channel-message` frames carry the server `eventId`; a client can pass its last-seen `lastEventId` on (re)subscribe and the server replays only the persisted events belonging to that subscription's stream (10-minute retention, written only while the channel is interested). A checkpoint that expired or belongs to a different stream reports `channel-replay-gap` and the subscription continues with live delivery.
|
|
2242
|
+
- **Live-only channels.** `registerWebsocketChannel(name, ChannelClass, {liveOnly: true})` declares a channel that must never reach the replay event log; marking one interested throws, and the persistence decision rejects it even if stale interest state exists.
|
|
2243
|
+
- **Replayable params.** Channel classes can override the static `replayableBroadcastParams(broadcastParams)` hook to control which broadcast params are persisted for replay (for example, stripping server-only values).
|
|
2244
|
+
|
|
2245
|
+
See [docs/websocket-channels.md](docs/websocket-channels.md) for the full wire protocol, V1/V2 gap semantics, and lifecycle guarantees.
|
|
2246
|
+
|
|
2227
2247
|
## Raw websocket handlers
|
|
2228
2248
|
|
|
2229
2249
|
If you need to accept custom websocket message formats (for example, a vendor that does not use the Velocious request/subscribe protocol), provide a `websocketMessageHandlerResolver` in your configuration. It receives the upgrade request and can return a handler object with `onOpen`, `onMessage`, `onClose`, and `onError` hooks:
|
package/build/configuration.js
CHANGED
|
@@ -408,6 +408,13 @@ export default class VelociousConfiguration {
|
|
|
408
408
|
* @type {Map<string, typeof import("./http-server/websocket-channel.js").default>} */
|
|
409
409
|
this._websocketChannelClasses = new Map()
|
|
410
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Channel types registered with `{liveOnly: true}`: their traffic is
|
|
413
|
+
* never persisted for replay, and `markChannelInterested` rejects the
|
|
414
|
+
* name.
|
|
415
|
+
* @type {Set<string>} */
|
|
416
|
+
this._liveOnlyWebsocketChannels = new Set()
|
|
417
|
+
|
|
411
418
|
/**
|
|
412
419
|
* Stores the websocket channel subscriptions value.
|
|
413
420
|
* @type {Map<string, Set<import("./http-server/websocket-channel.js").default>>} - channelType → live subscriptions across all sessions.
|
|
@@ -1126,6 +1133,7 @@ export default class VelociousConfiguration {
|
|
|
1126
1133
|
}
|
|
1127
1134
|
|
|
1128
1135
|
return {
|
|
1136
|
+
liveOnlyChannels: Array.from(this._liveOnlyWebsocketChannels),
|
|
1129
1137
|
pausedSessions: this._pausedWebsocketSessions.size,
|
|
1130
1138
|
registeredChannels: Array.from(this._websocketChannelClasses.keys()),
|
|
1131
1139
|
registeredConnections: Array.from(this._websocketConnectionClasses.keys()),
|
|
@@ -2966,12 +2974,15 @@ export default class VelociousConfiguration {
|
|
|
2966
2974
|
* Clients subscribe via `{type: "channel-subscribe", channelType: name, ...}`.
|
|
2967
2975
|
* @param {string} name - Client-facing channel type name.
|
|
2968
2976
|
* @param {typeof import("./http-server/websocket-channel.js").default} ChannelClass - Websocket channel class.
|
|
2977
|
+
* @param {{liveOnly?: boolean}} [options] - Registration options.
|
|
2969
2978
|
* @returns {void}
|
|
2970
2979
|
*/
|
|
2971
|
-
registerWebsocketChannel(name, ChannelClass) {
|
|
2980
|
+
registerWebsocketChannel(name, ChannelClass, {liveOnly = false} = {}) {
|
|
2972
2981
|
if (!name) throw new Error("Channel name is required")
|
|
2973
2982
|
if (!ChannelClass) throw new Error("ChannelClass is required")
|
|
2974
2983
|
this._websocketChannelClasses.set(name, ChannelClass)
|
|
2984
|
+
|
|
2985
|
+
if (liveOnly) this._liveOnlyWebsocketChannels.add(name)
|
|
2975
2986
|
}
|
|
2976
2987
|
|
|
2977
2988
|
/**
|
|
@@ -2983,6 +2994,17 @@ export default class VelociousConfiguration {
|
|
|
2983
2994
|
return this._websocketChannelClasses.get(name)
|
|
2984
2995
|
}
|
|
2985
2996
|
|
|
2997
|
+
/**
|
|
2998
|
+
* Whether a channel type was registered with `{liveOnly: true}`.
|
|
2999
|
+
* Live-only channels are never persisted for replay: the event-log
|
|
3000
|
+
* store's `markChannelInterested` throws for their names.
|
|
3001
|
+
* @param {string} name - Channel type name to look up.
|
|
3002
|
+
* @returns {boolean} - Whether the channel is declared live-only.
|
|
3003
|
+
*/
|
|
3004
|
+
isWebsocketChannelLiveOnly(name) {
|
|
3005
|
+
return this._liveOnlyWebsocketChannels.has(name)
|
|
3006
|
+
}
|
|
3007
|
+
|
|
2986
3008
|
/**
|
|
2987
3009
|
* Tracks a live channel subscription in the global routing registry.
|
|
2988
3010
|
* Called by the session when `canSubscribe()` resolves truthy; the
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import BaseCommand from "../../../../../../cli/base-command.js"
|
|
4
|
+
import { parseTimingManifestMergeArguments as parsePackageTimingManifestMergeArguments } from "@velocious/testing/node"
|
|
4
5
|
import fs from "node:fs/promises"
|
|
5
|
-
import path from "node:path"
|
|
6
6
|
import { writeTimingManifest } from "../../../../../../testing/test-profile-output.js"
|
|
7
7
|
import { mergeTestProfileTimingManifests } from "../../../../../../testing/timing-manifest.js"
|
|
8
8
|
|
|
@@ -51,24 +51,6 @@ export default class TestTimingManifestMerge extends BaseCommand {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
* Recognizes one output option spelling.
|
|
56
|
-
* @param {string} argument - Current argument.
|
|
57
|
-
* @param {string | undefined} nextArgument - Following argument.
|
|
58
|
-
* @returns {{matched: boolean, skipNext: boolean, value: string | undefined}} - Parsed output option.
|
|
59
|
-
*/
|
|
60
|
-
function timingManifestOutputArgument(argument, nextArgument) {
|
|
61
|
-
if (argument === "--output") {
|
|
62
|
-
return {matched: true, skipNext: true, value: nextArgument}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (argument.startsWith("--output=")) {
|
|
66
|
-
return {matched: true, skipNext: false, value: argument.slice("--output=".length)}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return {matched: false, skipNext: false, value: undefined}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
54
|
/**
|
|
73
55
|
* Parses strict merge arguments and resolves their paths.
|
|
74
56
|
* @param {string[]} processArgs - Raw CLI arguments, including command name.
|
|
@@ -76,33 +58,5 @@ function timingManifestOutputArgument(argument, nextArgument) {
|
|
|
76
58
|
* @returns {TimingManifestMergeArguments} - Validated resolved paths.
|
|
77
59
|
*/
|
|
78
60
|
export function parseTimingManifestMergeArguments(processArgs, cwd) {
|
|
79
|
-
|
|
80
|
-
const inputPaths = []
|
|
81
|
-
let outputPath
|
|
82
|
-
|
|
83
|
-
for (let index = 1; index < processArgs.length; index++) {
|
|
84
|
-
const argument = processArgs[index]
|
|
85
|
-
const outputArgument = timingManifestOutputArgument(argument, processArgs[index + 1])
|
|
86
|
-
|
|
87
|
-
if (outputArgument.matched) {
|
|
88
|
-
if (!outputArgument.value || outputArgument.value.startsWith("-")) throw new Error("Missing value for --output")
|
|
89
|
-
if (outputPath) throw new Error("--output may only be provided once")
|
|
90
|
-
outputPath = path.resolve(cwd, outputArgument.value)
|
|
91
|
-
if (outputArgument.skipNext) index++
|
|
92
|
-
continue
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (argument.startsWith("-")) throw new Error(`Unknown argument for ${commandName}: ${argument}`)
|
|
96
|
-
inputPaths.push(path.resolve(cwd, argument))
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!outputPath) throw new Error("--output is required")
|
|
100
|
-
if (inputPaths.length === 0) throw new Error("At least one rich test profile input is required")
|
|
101
|
-
|
|
102
|
-
const uniqueInputPaths = new Set(inputPaths)
|
|
103
|
-
|
|
104
|
-
if (uniqueInputPaths.size !== inputPaths.length) throw new Error("Each rich test profile input must be provided once")
|
|
105
|
-
if (uniqueInputPaths.has(outputPath)) throw new Error("Timing manifest output must not overwrite an input profile")
|
|
106
|
-
|
|
107
|
-
return {inputPaths, outputPath}
|
|
61
|
+
return parsePackageTimingManifestMergeArguments(processArgs.slice(1), {cwd})
|
|
108
62
|
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import BaseCommand from "../../../../cli/base-command.js"
|
|
4
|
-
import fs from "fs/promises"
|
|
5
4
|
import path from "node:path"
|
|
6
5
|
import picocolors from "picocolors"
|
|
7
6
|
import TestFilesFinder from "../../../../testing/test-files-finder.js"
|
|
8
7
|
import TestProfiler from "../../../../testing/test-profiler.js"
|
|
9
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
formatTestProfileSummary,
|
|
10
|
+
loadTimingManifest,
|
|
11
|
+
resolveTestProfileOptions,
|
|
12
|
+
writeTestProfileOutputs
|
|
13
|
+
} from "../../../../testing/test-profile-output.js"
|
|
10
14
|
import TestRunner from "../../../../testing/test-runner.js"
|
|
11
15
|
import TestSuiteSplitter from "../../../../testing/test-suite-splitter.js"
|
|
12
16
|
import { normalizeExamplePatterns, parseFilters } from "../../../../testing/test-filter-parser.js"
|
|
13
17
|
import {
|
|
14
18
|
canonicalTimingManifestPath,
|
|
15
|
-
timingManifestFileSetHash
|
|
16
|
-
validateTimingManifest
|
|
19
|
+
timingManifestFileSetHash
|
|
17
20
|
} from "../../../../testing/timing-manifest.js"
|
|
18
21
|
import { prepareSourcePeerPackage } from "../../source-peer-package.js"
|
|
19
22
|
|
|
@@ -44,8 +47,11 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
44
47
|
groupNumber,
|
|
45
48
|
profile,
|
|
46
49
|
profileJsonPath,
|
|
50
|
+
retries,
|
|
51
|
+
setupFiles,
|
|
47
52
|
timingManifestPath,
|
|
48
|
-
timingManifestOutputPath
|
|
53
|
+
timingManifestOutputPath,
|
|
54
|
+
timeoutMs
|
|
49
55
|
} = parseFilters(this.processArgs || [])
|
|
50
56
|
const profileOptions = resolveTestProfileOptions({
|
|
51
57
|
cwd: process.cwd(),
|
|
@@ -70,7 +76,7 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
70
76
|
|
|
71
77
|
/**
|
|
72
78
|
* Finalizes requested outputs once for every command outcome.
|
|
73
|
-
* @param {
|
|
79
|
+
* @param {import("@velocious/testing/node").TestProfileStatus} status - Run status.
|
|
74
80
|
* @returns {Promise<void>} - Resolves after requested outputs are written.
|
|
75
81
|
*/
|
|
76
82
|
const finalizeProfile = async (status) => {
|
|
@@ -157,7 +163,10 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
157
163
|
testFiles,
|
|
158
164
|
lineFilters: testFilesFinder.getLineFiltersByFile(),
|
|
159
165
|
examplePatterns: normalizeExamplePatterns(examplePatterns),
|
|
160
|
-
profiler
|
|
166
|
+
profiler,
|
|
167
|
+
retries,
|
|
168
|
+
setupFiles: setupFiles.map((setupFile) => path.resolve(process.cwd(), setupFile)),
|
|
169
|
+
timeoutMs
|
|
161
170
|
})
|
|
162
171
|
const activeTestRunner = testRunner
|
|
163
172
|
let signalHandled = false
|
|
@@ -272,68 +281,7 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
272
281
|
}
|
|
273
282
|
}
|
|
274
283
|
|
|
275
|
-
|
|
276
|
-
* Resolves and validates profiling paths before test discovery starts.
|
|
277
|
-
* @param {object} args - Raw profiling options.
|
|
278
|
-
* @param {string} args.cwd - Command working directory.
|
|
279
|
-
* @param {boolean} args.profile - Whether console profiling was requested.
|
|
280
|
-
* @param {string} [args.profileJsonPath] - Rich profile output path.
|
|
281
|
-
* @param {string} [args.timingManifestPath] - Timing manifest input path.
|
|
282
|
-
* @param {string} [args.timingManifestOutputPath] - Timing manifest output path.
|
|
283
|
-
* @returns {{profile: boolean, profileJsonPath: string | undefined, timingManifestPath: string | undefined, timingManifestOutputPath: string | undefined}} - Resolved profiling options.
|
|
284
|
-
*/
|
|
285
|
-
export function resolveTestProfileOptions({cwd, profile, profileJsonPath, timingManifestPath, timingManifestOutputPath}) {
|
|
286
|
-
const resolvedProfileJsonPath = profileJsonPath ? path.resolve(cwd, profileJsonPath) : undefined
|
|
287
|
-
const resolvedTimingManifestPath = timingManifestPath ? path.resolve(cwd, timingManifestPath) : undefined
|
|
288
|
-
const resolvedTimingManifestOutputPath = timingManifestOutputPath
|
|
289
|
-
? path.resolve(cwd, timingManifestOutputPath)
|
|
290
|
-
: undefined
|
|
291
|
-
|
|
292
|
-
if (resolvedProfileJsonPath && resolvedTimingManifestOutputPath && resolvedProfileJsonPath === resolvedTimingManifestOutputPath) {
|
|
293
|
-
throw new Error("Test profiling output paths must be different")
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (resolvedTimingManifestPath && (
|
|
297
|
-
resolvedProfileJsonPath === resolvedTimingManifestPath ||
|
|
298
|
-
resolvedTimingManifestOutputPath === resolvedTimingManifestPath
|
|
299
|
-
)) {
|
|
300
|
-
throw new Error("Test profiling outputs must not overwrite --timing-manifest input")
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
profile: profile || Boolean(resolvedProfileJsonPath || resolvedTimingManifestOutputPath),
|
|
305
|
-
profileJsonPath: resolvedProfileJsonPath,
|
|
306
|
-
timingManifestPath: resolvedTimingManifestPath,
|
|
307
|
-
timingManifestOutputPath: resolvedTimingManifestOutputPath
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Loads and validates an explicitly supplied plain JSON timing manifest.
|
|
313
|
-
* @param {string | undefined} timingManifestPath - Timing manifest path.
|
|
314
|
-
* @returns {Promise<Record<string, number> | undefined>} - Canonical manifest, or undefined when not requested.
|
|
315
|
-
*/
|
|
316
|
-
export async function loadTimingManifest(timingManifestPath) {
|
|
317
|
-
if (!timingManifestPath) return undefined
|
|
318
|
-
|
|
319
|
-
let content
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
content = await fs.readFile(timingManifestPath, "utf8")
|
|
323
|
-
} catch (error) {
|
|
324
|
-
throw new Error(`Failed to read timing manifest: ${timingManifestPath}`, {cause: error})
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
let parsed
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
parsed = JSON.parse(content)
|
|
331
|
-
} catch (error) {
|
|
332
|
-
throw new Error(`Failed to parse timing manifest: ${timingManifestPath}`, {cause: error})
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return validateTimingManifest(parsed, {source: `Timing manifest ${timingManifestPath}`})
|
|
336
|
-
}
|
|
284
|
+
export { loadTimingManifest, resolveTestProfileOptions }
|
|
337
285
|
|
|
338
286
|
/**
|
|
339
287
|
* Resolves how many slowest tests to report from the `VELOCIOUS_SLOW_TEST_COUNT`
|
|
@@ -389,6 +389,24 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
389
389
|
return broadcastParams?.model === this._modelName()
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Drops the server-only destroy-authorization snapshot before replay
|
|
394
|
+
* persistence. The snapshot is what makes replayed destroy events
|
|
395
|
+
* require a client resync, and the pre-delete row it captures must
|
|
396
|
+
* never be stored.
|
|
397
|
+
* @param {Record<string, import("./query.js").FrontendModelTransportValue> | null | undefined} broadcastParams - Params from `broadcastToChannel`.
|
|
398
|
+
* @returns {Record<string, import("./query.js").FrontendModelTransportValue> | null} - Persistable routing params.
|
|
399
|
+
*/
|
|
400
|
+
static replayableBroadcastParams(broadcastParams) {
|
|
401
|
+
if (!broadcastParams) return null
|
|
402
|
+
|
|
403
|
+
const replayableParams = {...broadcastParams}
|
|
404
|
+
|
|
405
|
+
delete replayableParams.destroyAuthorizationRecord
|
|
406
|
+
|
|
407
|
+
return replayableParams
|
|
408
|
+
}
|
|
409
|
+
|
|
392
410
|
/**
|
|
393
411
|
* Runs debug snapshot.
|
|
394
412
|
* @returns {Record<string, ReturnType<typeof JSON.parse>>} Debug-safe subscription details.
|
|
@@ -1843,8 +1843,13 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1843
1843
|
|
|
1844
1844
|
/**
|
|
1845
1845
|
* Replays missed events from the persistent event-log store for a
|
|
1846
|
-
* channel subscription that provided `lastEventId`.
|
|
1847
|
-
*
|
|
1846
|
+
* channel subscription that provided `lastEventId`. Delivery is
|
|
1847
|
+
* stream-scoped: each persisted event's broadcast params are
|
|
1848
|
+
* re-applied through the subscription's `matches()` — the same routing
|
|
1849
|
+
* decision live delivery uses — so the subscription only replays events
|
|
1850
|
+
* from its own stream of the channel. A checkpoint that belongs to a
|
|
1851
|
+
* different stream (or is no longer retained) yields
|
|
1852
|
+
* `channel-replay-gap` instead of cross-stream replay.
|
|
1848
1853
|
* @param {object} args - Options.
|
|
1849
1854
|
* @param {string} args.channelType - Channel type name (event-log key).
|
|
1850
1855
|
* @param {string} args.lastEventId - Client's last-seen event id.
|
|
@@ -1858,7 +1863,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1858
1863
|
|
|
1859
1864
|
const checkpoint = await store.getEventById({channel: channelType, id: lastEventId})
|
|
1860
1865
|
|
|
1861
|
-
if (!checkpoint) {
|
|
1866
|
+
if (!checkpoint || !this._replayEventMatchesSubscription({channelType, event: checkpoint, subscription})) {
|
|
1862
1867
|
this.sendJson({
|
|
1863
1868
|
type: "channel-replay-gap",
|
|
1864
1869
|
subscriptionId: subscription.subscriptionId,
|
|
@@ -1880,6 +1885,8 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1880
1885
|
for (const event of events) {
|
|
1881
1886
|
if (subscription.isClosed()) break
|
|
1882
1887
|
|
|
1888
|
+
if (!this._replayEventMatchesSubscription({channelType, event, subscription})) continue
|
|
1889
|
+
|
|
1883
1890
|
if (await subscription._requiresReplayGap(event.payload)) {
|
|
1884
1891
|
this.sendJson({
|
|
1885
1892
|
type: "channel-replay-gap",
|
|
@@ -1891,11 +1898,41 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1891
1898
|
|
|
1892
1899
|
await subscription.deliverBroadcast(
|
|
1893
1900
|
/** @type {import("../websocket-channel.js").WebsocketJsonValue} */ (event.payload),
|
|
1894
|
-
{
|
|
1901
|
+
{
|
|
1902
|
+
...(event.params !== null ? {broadcastParams: event.params} : {}),
|
|
1903
|
+
eventId: event.id
|
|
1904
|
+
}
|
|
1895
1905
|
)
|
|
1896
1906
|
}
|
|
1897
1907
|
}
|
|
1898
1908
|
|
|
1909
|
+
/**
|
|
1910
|
+
* Whether a persisted replay event belongs to this subscription's
|
|
1911
|
+
* stream. A `matches()` failure means the stream membership cannot be
|
|
1912
|
+
* proven, so the event is treated as not matching — the same isolation
|
|
1913
|
+
* live delivery applies to a broken `matches()`.
|
|
1914
|
+
* @param {object} args - Options.
|
|
1915
|
+
* @param {string} args.channelType - Channel type name.
|
|
1916
|
+
* @param {{params: Record<string, ReturnType<typeof JSON.parse>> | null}} args.event - Persisted replay event.
|
|
1917
|
+
* @param {import("../websocket-channel.js").default} args.subscription - Live subscription.
|
|
1918
|
+
* @returns {boolean} - Whether the event belongs to the subscription's stream.
|
|
1919
|
+
*/
|
|
1920
|
+
_replayEventMatchesSubscription({channelType, event, subscription}) {
|
|
1921
|
+
try {
|
|
1922
|
+
return Boolean(subscription.matches(event.params || {}))
|
|
1923
|
+
} catch (caughtError) {
|
|
1924
|
+
const error = this._reportUnexpectedDispatchError(caughtError, {
|
|
1925
|
+
channelType,
|
|
1926
|
+
stage: "websocket-channel-replay",
|
|
1927
|
+
subscriptionId: subscription.subscriptionId
|
|
1928
|
+
})
|
|
1929
|
+
|
|
1930
|
+
this.logger.error(() => [`Websocket replay subscription ${subscription.subscriptionId} matches() threw`, error])
|
|
1931
|
+
|
|
1932
|
+
return false
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1899
1936
|
/**
|
|
1900
1937
|
* Handles `{type: "channel-unsubscribe"}` from the client — calls
|
|
1901
1938
|
* `unsubscribed()` and sends `channel-unsubscribed`.
|
|
@@ -94,6 +94,20 @@ export default class VelociousWebsocketChannel {
|
|
|
94
94
|
*/
|
|
95
95
|
matches(..._broadcastArgs) { return true }
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Returns the broadcast params that may be persisted for replay.
|
|
99
|
+
* Persisted params are re-applied through `matches(broadcastParams)`
|
|
100
|
+
* when replaying missed events, so they must be JSON-serializable and
|
|
101
|
+
* safe to store. Override when `broadcastParams` carries server-only
|
|
102
|
+
* values that must never reach the event log (e.g. authorization
|
|
103
|
+
* snapshots).
|
|
104
|
+
* @param {WebsocketParams | null | undefined} broadcastParams - Params passed to `broadcastToChannel`.
|
|
105
|
+
* @returns {WebsocketParams | null} - Params to persist, or null to store none.
|
|
106
|
+
*/
|
|
107
|
+
static replayableBroadcastParams(broadcastParams) {
|
|
108
|
+
return broadcastParams ?? null
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
/**
|
|
98
112
|
* Whether replaying a persisted broadcast would require a client resync.
|
|
99
113
|
* Subclasses override this when replay storage deliberately omits metadata
|