velocious 1.0.667 → 1.0.669

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.
Files changed (112) hide show
  1. package/README.md +34 -9
  2. package/build/configuration.js +23 -1
  3. package/build/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
  4. package/build/environment-handlers/node/cli/commands/test.js +17 -69
  5. package/build/frontend-models/websocket-channel.js +18 -0
  6. package/build/http-server/client/websocket-session.js +41 -4
  7. package/build/http-server/websocket-channel.js +14 -0
  8. package/build/http-server/websocket-event-log-store.js +85 -12
  9. package/build/http-server/websocket-events-host.js +14 -6
  10. package/build/src/configuration.d.ts +18 -1
  11. package/build/src/configuration.d.ts.map +1 -1
  12. package/build/src/configuration.js +22 -2
  13. package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.d.ts.map +1 -1
  14. package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +3 -47
  15. package/build/src/environment-handlers/node/cli/commands/test.d.ts +2 -28
  16. package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
  17. package/build/src/environment-handlers/node/cli/commands/test.js +10 -61
  18. package/build/src/frontend-models/websocket-channel.d.ts +9 -0
  19. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  20. package/build/src/frontend-models/websocket-channel.js +16 -1
  21. package/build/src/http-server/client/websocket-session.d.ts +25 -2
  22. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  23. package/build/src/http-server/client/websocket-session.js +40 -5
  24. package/build/src/http-server/websocket-channel.d.ts +11 -0
  25. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  26. package/build/src/http-server/websocket-channel.js +14 -1
  27. package/build/src/http-server/websocket-event-log-store.d.ts +60 -41
  28. package/build/src/http-server/websocket-event-log-store.d.ts.map +1 -1
  29. package/build/src/http-server/websocket-event-log-store.js +79 -12
  30. package/build/src/http-server/websocket-events-host.d.ts +10 -3
  31. package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
  32. package/build/src/http-server/websocket-events-host.js +14 -7
  33. package/build/src/testing/expect-utils.d.ts +1 -51
  34. package/build/src/testing/expect-utils.d.ts.map +1 -1
  35. package/build/src/testing/expect-utils.js +2 -205
  36. package/build/src/testing/expect.d.ts +1 -171
  37. package/build/src/testing/expect.d.ts.map +1 -1
  38. package/build/src/testing/expect.js +2 -667
  39. package/build/src/testing/test-files-finder.d.ts +13 -116
  40. package/build/src/testing/test-files-finder.d.ts.map +1 -1
  41. package/build/src/testing/test-files-finder.js +26 -296
  42. package/build/src/testing/test-filter-parser.d.ts +32 -22
  43. package/build/src/testing/test-filter-parser.d.ts.map +1 -1
  44. package/build/src/testing/test-filter-parser.js +25 -217
  45. package/build/src/testing/test-profile-activity.d.ts +1 -6
  46. package/build/src/testing/test-profile-activity.d.ts.map +1 -1
  47. package/build/src/testing/test-profile-activity.js +2 -12
  48. package/build/src/testing/test-profile-output.d.ts +1 -40
  49. package/build/src/testing/test-profile-output.d.ts.map +1 -1
  50. package/build/src/testing/test-profile-output.js +2 -217
  51. package/build/src/testing/test-profiler.d.ts +15 -659
  52. package/build/src/testing/test-profiler.d.ts.map +1 -1
  53. package/build/src/testing/test-profiler.js +35 -833
  54. package/build/src/testing/test-runner.d.ts +10 -1
  55. package/build/src/testing/test-runner.d.ts.map +1 -1
  56. package/build/src/testing/test-runner.js +26 -4
  57. package/build/src/testing/test-suite-splitter.d.ts +1 -128
  58. package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
  59. package/build/src/testing/test-suite-splitter.js +2 -259
  60. package/build/src/testing/test.d.ts +1 -13
  61. package/build/src/testing/test.d.ts.map +1 -1
  62. package/build/src/testing/test.js +2 -14
  63. package/build/src/testing/timing-manifest.d.ts +5 -93
  64. package/build/src/testing/timing-manifest.d.ts.map +1 -1
  65. package/build/src/testing/timing-manifest.js +4 -283
  66. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
  67. package/build/src/testing/velocious-attempt-executor.js +8 -2
  68. package/build/testing/expect-utils.js +1 -242
  69. package/build/testing/expect.js +1 -760
  70. package/build/testing/test-files-finder.js +29 -345
  71. package/build/testing/test-filter-parser.js +29 -248
  72. package/build/testing/test-profile-activity.js +1 -12
  73. package/build/testing/test-profile-output.js +8 -253
  74. package/build/testing/test-profiler.js +38 -895
  75. package/build/testing/test-runner.js +33 -9
  76. package/build/testing/test-suite-splitter.js +1 -301
  77. package/build/testing/test.js +3 -14
  78. package/build/testing/timing-manifest.js +10 -344
  79. package/build/testing/velocious-attempt-executor.js +7 -1
  80. package/package.json +3 -4
  81. package/scripts/test-browser.js +29 -6
  82. package/scripts/verify-docker-dev-environment.js +13 -1
  83. package/src/configuration.js +23 -1
  84. package/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
  85. package/src/environment-handlers/node/cli/commands/test.js +17 -69
  86. package/src/frontend-models/websocket-channel.js +18 -0
  87. package/src/http-server/client/websocket-session.js +41 -4
  88. package/src/http-server/websocket-channel.js +14 -0
  89. package/src/http-server/websocket-event-log-store.js +85 -12
  90. package/src/http-server/websocket-events-host.js +14 -6
  91. package/src/testing/expect-utils.js +1 -242
  92. package/src/testing/expect.js +1 -760
  93. package/src/testing/test-files-finder.js +29 -345
  94. package/src/testing/test-filter-parser.js +29 -248
  95. package/src/testing/test-profile-activity.js +1 -12
  96. package/src/testing/test-profile-output.js +8 -253
  97. package/src/testing/test-profiler.js +38 -895
  98. package/src/testing/test-runner.js +33 -9
  99. package/src/testing/test-suite-splitter.js +1 -301
  100. package/src/testing/test.js +3 -14
  101. package/src/testing/timing-manifest.js +10 -344
  102. package/src/testing/velocious-attempt-executor.js +7 -1
  103. package/build/src/testing/base-expect.d.ts +0 -15
  104. package/build/src/testing/base-expect.d.ts.map +0 -1
  105. package/build/src/testing/base-expect.js +0 -16
  106. package/build/src/testing/expect-to-change.d.ts +0 -32
  107. package/build/src/testing/expect-to-change.d.ts.map +0 -1
  108. package/build/src/testing/expect-to-change.js +0 -46
  109. package/build/testing/base-expect.js +0 -17
  110. package/build/testing/expect-to-change.js +0 -55
  111. package/src/testing/base-expect.js +0 -17
  112. package/src/testing/expect-to-change.js +0 -55
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, and the newest published provider CLIs; it is source-independent — no project source is copied and no project dependencies are installed at image build time.
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,15 @@ 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.12` is the declaration and execution engine. Compatible
183
- installed copies share one protocol-1/schema-3 default registry. Velocious adapts each
184
- package-owned attempt with its database, request, profiling, and cleanup behavior; the
185
- existing Velocious facade exports the same declaration DSL and remains supported.
183
+ `@velocious/testing` `0.0.17` owns framework-neutral expectations, matchers,
184
+ equality, change observation, mocks, discovery, filtering, execution, reporting,
185
+ deterministic weighted grouping, profiling, and timing manifests. Compatible
186
+ installed copies share one protocol-1/schema-3 default registry.
187
+ Velocious retains application/configuration startup, database and transaction
188
+ lifecycle, browser execution, environment handling, framework profile spans, and
189
+ report translation. Existing Velocious import paths remain supported as thin
190
+ delegating compatibility facades. Package diagnostics are canonical when legacy
191
+ facades differ only in wording or formatting.
186
192
 
187
193
  ```js
188
194
  import {describe, expect, it} from "@velocious/testing"
@@ -245,7 +251,9 @@ export default async function configureTesting() {
245
251
  }
246
252
  ```
247
253
 
248
- Retry flaky tests by setting a retry count on the test args.
254
+ Retry flaky tests by setting a retry count on the test args, or set CLI defaults for
255
+ retries and lifecycle timeout. Import repeatable global setup before the testing
256
+ configuration and test declarations with `--setup`.
249
257
 
250
258
  ```js
251
259
  describe("Tasks", () => {
@@ -253,6 +261,10 @@ it("retries a flaky check", {retry: 2}, async () => {})
253
261
  })
254
262
  ```
255
263
 
264
+ ```bash
265
+ npx velocious test --retry=2 --timeout=30000 --setup=spec/setup.js spec/
266
+ ```
267
+
256
268
  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
269
 
258
270
  ```bash
@@ -295,7 +307,7 @@ npx velocious test:timing-manifest:merge --output tmp/test-timings.json \
295
307
  See [test profiling](docs/test-profiling.md) for lifecycle accounting, custom
296
308
  activity spans, schema, and privacy guarantees.
297
309
 
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, profiling, and cleanup behavior.
310
+ 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
311
 
300
312
  ```js
301
313
  import {waitForEvent} from "velocious/build/src/testing/test.js"
@@ -2224,6 +2236,16 @@ socket.send(JSON.stringify({
2224
2236
  }))
2225
2237
  ```
2226
2238
 
2239
+ ### V2 channel registration, replay, and live-only channels
2240
+
2241
+ `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)`.
2242
+
2243
+ - **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.
2244
+ - **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.
2245
+ - **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).
2246
+
2247
+ See [docs/websocket-channels.md](docs/websocket-channels.md) for the full wire protocol, V1/V2 gap semantics, and lifecycle guarantees.
2248
+
2227
2249
  ## Raw websocket handlers
2228
2250
 
2229
2251
  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:
@@ -2275,8 +2297,11 @@ npx velocious test
2275
2297
 
2276
2298
  Test declarations can be imported from `@velocious/testing`; the compatibility
2277
2299
  `velocious/build/src/testing/test.js` facade uses the same package registry and remains
2278
- supported. Under Velocious, ordinary callbacks receive `testArgs`, while `it.each`
2279
- callbacks receive their row arguments followed by `testArgs`.
2300
+ supported. Its `expect`, `arrayContaining`, and `objectContaining` exports, along
2301
+ with the retained deep `expect.js` and `expect-utils.js` paths, delegate directly
2302
+ to the browser-safe package implementation. Under Velocious, ordinary callbacks
2303
+ receive `testArgs`, while `it.each` callbacks receive their row arguments followed
2304
+ by `testArgs`.
2280
2305
 
2281
2306
  If you are developing on Velocious, you can run the tests with:
2282
2307
 
@@ -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
- const commandName = processArgs[0] || "test:timing-manifest:merge"
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 { formatTestProfileSummary, writeTestProfileOutputs } from "../../../../testing/test-profile-output.js"
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 {string} status - Run status.
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`. Sends each
1847
- * missed event as a `channel-message` with `replayed: true`.
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
- {eventId: event.id}
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