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
|
@@ -1,346 +1,12 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* @property {number} groups - Complete shard count.
|
|
14
|
-
* @property {string} pathBase - Profiling path-base semantics.
|
|
15
|
-
* @property {string} testFileSetHash - Complete canonical file-set identity.
|
|
16
|
-
* @property {TimingManifest} timingManifest - Canonical shard timing map.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* TestProfileTimingManifestInput type.
|
|
21
|
-
* @typedef {object} TestProfileTimingManifestInput
|
|
22
|
-
* @property {ReturnType<typeof JSON.parse>} profile - Parsed rich test profile.
|
|
23
|
-
* @property {string} source - Human-readable input source for validation errors.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Canonicalizes a timing-manifest path relative to its profiling base.
|
|
28
|
-
* @param {string} filePath - Candidate relative path.
|
|
29
|
-
* @returns {string} - Portable canonical path.
|
|
30
|
-
*/
|
|
31
|
-
export function canonicalTimingManifestPath(filePath) {
|
|
32
|
-
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
33
|
-
throw new Error("Timing manifest keys must be non-empty relative paths")
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const portablePath = filePath.replaceAll("\\", "/")
|
|
37
|
-
|
|
38
|
-
if (portablePath.startsWith("/") || /^[A-Za-z]:/.test(portablePath)) {
|
|
39
|
-
throw new Error(`Timing manifest key must be a relative path: ${filePath}`)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const segments = []
|
|
43
|
-
|
|
44
|
-
for (const segment of portablePath.split("/")) {
|
|
45
|
-
if (!segment || segment === ".") continue
|
|
46
|
-
|
|
47
|
-
if (segment === "..") {
|
|
48
|
-
if (segments.length === 0) {
|
|
49
|
-
throw new Error(`Timing manifest key must be a non-escaping relative path: ${filePath}`)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
segments.pop()
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
segments.push(segment)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (segments.length === 0) {
|
|
60
|
-
throw new Error(`Timing manifest key must be a non-empty relative path: ${filePath}`)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return segments.join("/")
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Compares timing-manifest paths by JavaScript code units without locale rules.
|
|
68
|
-
* @param {string} filePathA - First path.
|
|
69
|
-
* @param {string} filePathB - Second path.
|
|
70
|
-
* @returns {number} - Negative, zero, or positive ordering result.
|
|
71
|
-
*/
|
|
72
|
-
export function compareTimingManifestPaths(filePathA, filePathB) {
|
|
73
|
-
if (filePathA === filePathB) return 0
|
|
74
|
-
|
|
75
|
-
return filePathA < filePathB ? -1 : 1
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Validates and sorts a plain timing manifest.
|
|
80
|
-
* @param {ReturnType<typeof JSON.parse>} timingManifest - Parsed timing manifest.
|
|
81
|
-
* @param {{source?: string}} [options] - Validation context.
|
|
82
|
-
* @returns {TimingManifest} - Canonical sorted timing manifest.
|
|
83
|
-
*/
|
|
84
|
-
export function validateTimingManifest(timingManifest, {source = "timing manifest"} = {}) {
|
|
85
|
-
if (!timingManifest || typeof timingManifest !== "object" || Array.isArray(timingManifest)) {
|
|
86
|
-
throw new Error(`${source} must be a plain JSON object mapping relative paths to durations`)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** @type {Map<string, {duration: number, originalPath: string}>} */
|
|
90
|
-
const entries = new Map()
|
|
91
|
-
|
|
92
|
-
for (const [originalPath, duration] of Object.entries(timingManifest)) {
|
|
93
|
-
if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0) {
|
|
94
|
-
throw new Error(`${source} has an invalid duration for ${originalPath}`)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const canonicalPath = canonicalTimingManifestPath(originalPath)
|
|
98
|
-
const existing = entries.get(canonicalPath)
|
|
99
|
-
|
|
100
|
-
if (existing) {
|
|
101
|
-
throw new Error(`${source} has a normalized path collision between ${existing.originalPath} and ${originalPath}`)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
entries.set(canonicalPath, {duration, originalPath})
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return Object.fromEntries(
|
|
108
|
-
[...entries.entries()]
|
|
109
|
-
.sort(([filePathA], [filePathB]) => compareTimingManifestPaths(filePathA, filePathB))
|
|
110
|
-
.map(([filePath, entry]) => [filePath, entry.duration])
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Returns an opaque deterministic identity for a complete canonical test-file set.
|
|
116
|
-
* @param {string[]} filePaths - Paths relative to one profiling base.
|
|
117
|
-
* @returns {string} - SHA-256 file-set identity.
|
|
118
|
-
*/
|
|
119
|
-
export function timingManifestFileSetHash(filePaths) {
|
|
120
|
-
const pathManifest = Object.fromEntries(filePaths.map((filePath) => [filePath, 0]))
|
|
121
|
-
const canonicalPaths = Object.keys(validateTimingManifest(pathManifest, {source: "test file set"}))
|
|
122
|
-
const identity = `velocious.test-file-set.v1\0${canonicalPaths.join("\0")}`
|
|
123
|
-
|
|
124
|
-
return `sha256:${sha256Hex(identity)}`
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Validates that a parsed JSON value is a non-array object.
|
|
129
|
-
* @param {ReturnType<typeof JSON.parse>} value - Candidate object.
|
|
130
|
-
* @param {string} message - Validation error message.
|
|
131
|
-
* @returns {void}
|
|
132
|
-
*/
|
|
133
|
-
function assertJsonObject(value, message) {
|
|
134
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(message)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Requires unfiltered, non-focused profile selection metadata.
|
|
139
|
-
* @param {ReturnType<typeof JSON.parse>} selection - Rich profile selection.
|
|
140
|
-
* @param {string} source - Input source.
|
|
141
|
-
* @returns {void}
|
|
142
|
-
*/
|
|
143
|
-
function assertCompleteSelection(selection, source) {
|
|
144
|
-
if (selection.focused !== false) throw new Error(`${source} must not be a focused test profile`)
|
|
145
|
-
|
|
146
|
-
const selectionFilters = [
|
|
147
|
-
[selection.includeTagCount, 0],
|
|
148
|
-
[selection.excludeTagCount, 0],
|
|
149
|
-
[selection.hasExampleFilters, false],
|
|
150
|
-
[selection.hasLineFilters, false]
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
if (selectionFilters.some(([value, expected]) => value !== expected)) {
|
|
154
|
-
throw new Error(`${source} must not be a filtered test profile`)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Validates shard numbering metadata.
|
|
160
|
-
* @param {ReturnType<typeof JSON.parse>} shard - Candidate shard selection.
|
|
161
|
-
* @param {string} source - Input source.
|
|
162
|
-
* @returns {{groupNumber: number, groups: number}} - Validated shard numbers.
|
|
163
|
-
*/
|
|
164
|
-
function validatedShardNumbers(shard, source) {
|
|
165
|
-
assertJsonObject(shard, `${source} is missing shard metadata`)
|
|
166
|
-
|
|
167
|
-
if (!Number.isInteger(shard.groups) || shard.groups < 1) {
|
|
168
|
-
throw new Error(`${source} has an invalid shard group count`)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!Number.isInteger(shard.groupNumber) || shard.groupNumber < 1 || shard.groupNumber > shard.groups) {
|
|
172
|
-
throw new Error(`${source} has an invalid shard number`)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return {groupNumber: shard.groupNumber, groups: shard.groups}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Validates a non-negative integer selection count.
|
|
180
|
-
* @param {ReturnType<typeof JSON.parse>} value - Candidate count.
|
|
181
|
-
* @param {string} message - Validation error message.
|
|
182
|
-
* @returns {number} - Validated count.
|
|
183
|
-
*/
|
|
184
|
-
function validatedSelectionCount(value, message) {
|
|
185
|
-
if (!Number.isInteger(value) || value < 0) throw new Error(message)
|
|
186
|
-
|
|
187
|
-
return value
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Validates the selection identity used across shard profiles.
|
|
192
|
-
* @param {ReturnType<typeof JSON.parse>} selection - Rich profile selection.
|
|
193
|
-
* @param {string} source - Input source.
|
|
194
|
-
* @returns {{pathBase: string, testFileSetHash: string}} - Validated identity.
|
|
195
|
-
*/
|
|
196
|
-
function validatedSelectionIdentity(selection, source) {
|
|
197
|
-
if (!["configuration-directory", "test-directory"].includes(selection.pathBase)) {
|
|
198
|
-
throw new Error(`${source} has an invalid timing manifest path base`)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (typeof selection.testFileSetHash !== "string" || !/^sha256:[a-f0-9]{64}$/.test(selection.testFileSetHash)) {
|
|
202
|
-
throw new Error(`${source} has an invalid test file set identity`)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return {pathBase: selection.pathBase, testFileSetHash: selection.testFileSetHash}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Validates one rich profile and returns its aggregation contract.
|
|
210
|
-
* @param {TestProfileTimingManifestInput} input - Profile input.
|
|
211
|
-
* @returns {ValidatedProfileShard} - Validated shard contract.
|
|
212
|
-
*/
|
|
213
|
-
function validatedProfileShard(input) {
|
|
214
|
-
const {profile, source} = input
|
|
215
|
-
|
|
216
|
-
assertJsonObject(profile, `${source} must be a rich Velocious test profile`)
|
|
217
|
-
|
|
218
|
-
if (profile.schema !== "velocious.test-profile" || profile.schemaVersion !== 1) {
|
|
219
|
-
throw new Error(`${source} has an incompatible Velocious test profile schema`)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (profile.status !== "passed") throw new Error(`${source} must have passed status for timing aggregation`)
|
|
223
|
-
|
|
224
|
-
const selection = profile.selection
|
|
225
|
-
|
|
226
|
-
assertJsonObject(selection, `${source} is missing test profile selection metadata`)
|
|
227
|
-
assertCompleteSelection(selection, source)
|
|
228
|
-
const {groupNumber, groups} = validatedShardNumbers(selection.shard, source)
|
|
229
|
-
const discoveredFileCount = validatedSelectionCount(
|
|
230
|
-
selection.discoveredFileCount,
|
|
231
|
-
`${source} has an invalid pre-shard discovered file count`
|
|
232
|
-
)
|
|
233
|
-
const fileCount = validatedSelectionCount(
|
|
234
|
-
selection.fileCount,
|
|
235
|
-
`${source} has an invalid post-shard file count`
|
|
236
|
-
)
|
|
237
|
-
const {pathBase, testFileSetHash} = validatedSelectionIdentity(selection, source)
|
|
238
|
-
|
|
239
|
-
const timingManifest = validateTimingManifest(profile.timingManifest, {source: `${source} timing manifest`})
|
|
240
|
-
|
|
241
|
-
if (Object.keys(timingManifest).length !== fileCount) {
|
|
242
|
-
throw new Error(`${source} timing manifest does not match its post-shard file count`)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
discoveredFileCount,
|
|
247
|
-
fileCount,
|
|
248
|
-
groupNumber,
|
|
249
|
-
groups,
|
|
250
|
-
pathBase,
|
|
251
|
-
testFileSetHash,
|
|
252
|
-
timingManifest
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Requires one shard to describe the same complete selection as the first.
|
|
258
|
-
* @param {ValidatedProfileShard} shard - Candidate shard.
|
|
259
|
-
* @param {ValidatedProfileShard} expected - First shard contract.
|
|
260
|
-
* @param {string} source - Candidate source.
|
|
261
|
-
* @returns {void}
|
|
262
|
-
*/
|
|
263
|
-
function assertCompatibleShard(shard, expected, source) {
|
|
264
|
-
if (shard.groups !== expected.groups) throw new Error(`${source} has a different shard group count`)
|
|
265
|
-
if (shard.pathBase !== expected.pathBase) throw new Error(`${source} has a different timing manifest path base`)
|
|
266
|
-
if (shard.discoveredFileCount !== expected.discoveredFileCount) throw new Error(`${source} has a different discovered file count`)
|
|
267
|
-
if (shard.testFileSetHash !== expected.testFileSetHash) throw new Error(`${source} has a different test file set identity`)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Adds one validated shard timing map without allowing duplicate canonical keys.
|
|
272
|
-
* @param {object} args - Merge state.
|
|
273
|
-
* @param {Map<string, {duration: number, source: string}>} args.mergedEntries - Destination timing entries.
|
|
274
|
-
* @param {ValidatedProfileShard} args.shard - Candidate shard.
|
|
275
|
-
* @param {string} args.source - Candidate source.
|
|
276
|
-
* @returns {void}
|
|
277
|
-
*/
|
|
278
|
-
function mergeShardTimingManifest({mergedEntries, shard, source}) {
|
|
279
|
-
for (const [filePath, duration] of Object.entries(shard.timingManifest)) {
|
|
280
|
-
const existingEntry = mergedEntries.get(filePath)
|
|
281
|
-
|
|
282
|
-
if (existingEntry) {
|
|
283
|
-
throw new Error(`Duplicate timing path ${filePath} in ${existingEntry.source} and ${source}`)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
mergedEntries.set(filePath, {duration, source})
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Merges a complete compatible set of rich Velocious shard profiles.
|
|
292
|
-
* @param {TestProfileTimingManifestInput[]} inputs - Parsed profile documents and sources.
|
|
293
|
-
* @returns {TimingManifest} - Complete sorted plain timing manifest.
|
|
294
|
-
*/
|
|
295
|
-
export function mergeTestProfileTimingManifests(inputs) {
|
|
296
|
-
if (inputs.length === 0) throw new Error("At least one rich test profile is required")
|
|
297
|
-
|
|
298
|
-
const shards = inputs.map((input) => validatedProfileShard(input))
|
|
299
|
-
const expected = shards[0]
|
|
300
|
-
/** @type {Map<number, string>} */
|
|
301
|
-
const shardSources = new Map()
|
|
302
|
-
/** @type {Map<string, {duration: number, source: string}>} */
|
|
303
|
-
const mergedEntries = new Map()
|
|
304
|
-
let selectedFileCount = 0
|
|
305
|
-
|
|
306
|
-
for (let index = 0; index < shards.length; index++) {
|
|
307
|
-
const shard = shards[index]
|
|
308
|
-
const source = inputs[index].source
|
|
309
|
-
|
|
310
|
-
assertCompatibleShard(shard, expected, source)
|
|
311
|
-
|
|
312
|
-
const existingShardSource = shardSources.get(shard.groupNumber)
|
|
313
|
-
|
|
314
|
-
if (existingShardSource) {
|
|
315
|
-
throw new Error(`Duplicate shard ${shard.groupNumber} in ${existingShardSource} and ${source}`)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
shardSources.set(shard.groupNumber, source)
|
|
319
|
-
selectedFileCount += shard.fileCount
|
|
320
|
-
mergeShardTimingManifest({mergedEntries, shard, source})
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const missingShardNumbers = []
|
|
324
|
-
|
|
325
|
-
for (let groupNumber = 1; groupNumber <= expected.groups; groupNumber++) {
|
|
326
|
-
if (!shardSources.has(groupNumber)) missingShardNumbers.push(groupNumber)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (missingShardNumbers.length > 0) {
|
|
330
|
-
throw new Error(`Missing shard profiles: ${missingShardNumbers.join(", ")}`)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (selectedFileCount !== expected.discoveredFileCount || mergedEntries.size !== expected.discoveredFileCount) {
|
|
334
|
-
throw new Error("Merged timing manifest does not cover the complete file universe")
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const merged = Object.fromEntries(
|
|
338
|
-
[...mergedEntries].map(([filePath, entry]) => [filePath, entry.duration])
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
if (timingManifestFileSetHash(Object.keys(merged)) !== expected.testFileSetHash) {
|
|
342
|
-
throw new Error("Merged timing manifest does not match the complete file universe")
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return validateTimingManifest(merged, {source: "merged timing manifest"})
|
|
346
|
-
}
|
|
3
|
+
/** @typedef {import("@velocious/testing/node").TimingManifest} TimingManifest */
|
|
4
|
+
/** @typedef {import("@velocious/testing/node").TestProfileTimingManifestInput} TestProfileTimingManifestInput */
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
canonicalTimingManifestPath,
|
|
8
|
+
compareTimingManifestPaths,
|
|
9
|
+
mergeTestProfileTimingManifests,
|
|
10
|
+
timingManifestFileSetHash,
|
|
11
|
+
validateTimingManifest
|
|
12
|
+
} from "@velocious/testing/node"
|
|
@@ -148,10 +148,16 @@ export default class VelociousAttemptExecutor {
|
|
|
148
148
|
await testRunner.registerTransactionalTenant(args, transactionalTenantRegistrations)
|
|
149
149
|
}
|
|
150
150
|
const profiler = testRunner._profiler
|
|
151
|
+
const profileTestData = {
|
|
152
|
+
...test,
|
|
153
|
+
filePath: testData.filePath,
|
|
154
|
+
line: testData.line,
|
|
155
|
+
ownerFilePath: testData.ownerFilePath ?? metadata.ownerFilePath
|
|
156
|
+
}
|
|
151
157
|
const profileAttempt = profiler?.startAttempt({
|
|
152
158
|
descriptions,
|
|
153
159
|
attemptNumber,
|
|
154
|
-
testData,
|
|
160
|
+
testData: profileTestData,
|
|
155
161
|
testDescription
|
|
156
162
|
})
|
|
157
163
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"velocious": "build/bin/velocious.js"
|
|
4
4
|
},
|
|
5
5
|
"name": "velocious",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.668",
|
|
7
7
|
"main": "build/index.js",
|
|
8
8
|
"types": "build/index.d.ts",
|
|
9
9
|
"files": [
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"ws": "^8.21.3"
|
|
92
92
|
},
|
|
93
93
|
"peerDependencies": {
|
|
94
|
-
"@velocious/testing": "^0.0.
|
|
94
|
+
"@velocious/testing": "^0.0.14",
|
|
95
95
|
"react": ">=18",
|
|
96
96
|
"smtp-connection": "^4.0.2",
|
|
97
97
|
"sqlite": "^5.1.1",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"@types/react-dom": "^19.2.3",
|
|
122
122
|
"@types/strftime": "^0.9.8",
|
|
123
123
|
"@types/ws": "^8.18.1",
|
|
124
|
-
"@velocious/testing": "0.0.
|
|
124
|
+
"@velocious/testing": "0.0.14",
|
|
125
125
|
"chokidar-cli": "^3.0.0",
|
|
126
126
|
"cpy-cli": "^7.0.0",
|
|
127
127
|
"esbuild": "^0.28.0",
|
package/scripts/test-browser.js
CHANGED
|
@@ -268,7 +268,7 @@ async function resolveConfiguration({testFilesRequireContextCallback} = {}) {
|
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
270
|
* @param {string[]} processArgs - Process args.
|
|
271
|
-
* @returns {Promise<{directory: string, testFiles: string[], lineFilters: Record<string, number[]>, includeTags: string[], excludeTags: string[], examplePatterns: RegExp[]}>} - Test data.
|
|
271
|
+
* @returns {Promise<{directory: string, testFiles: string[], lineFilters: Record<string, number[]>, includeTags: string[], excludeTags: string[], examplePatterns: RegExp[], retries: number | undefined, setupFiles: string[], timeoutMs: number | undefined}>} - Test data.
|
|
272
272
|
*/
|
|
273
273
|
async function resolveTests(processArgs) {
|
|
274
274
|
const directory = process.env.VELOCIOUS_TEST_DIR
|
|
@@ -278,7 +278,15 @@ async function resolveTests(processArgs) {
|
|
|
278
278
|
? [directory]
|
|
279
279
|
: [`${rootDir}/__tests__`, `${rootDir}/tests`, `${rootDir}/spec`]
|
|
280
280
|
|
|
281
|
-
const {
|
|
281
|
+
const {
|
|
282
|
+
includeTags,
|
|
283
|
+
excludeTags,
|
|
284
|
+
examplePatterns,
|
|
285
|
+
filteredProcessArgs,
|
|
286
|
+
retries,
|
|
287
|
+
setupFiles,
|
|
288
|
+
timeoutMs
|
|
289
|
+
} = parseFilters(processArgs)
|
|
282
290
|
const testFilesFinder = new TestFilesFinder({
|
|
283
291
|
directory,
|
|
284
292
|
directories,
|
|
@@ -298,7 +306,10 @@ async function resolveTests(processArgs) {
|
|
|
298
306
|
lineFilters: testFilesFinder.getLineFiltersByFile(),
|
|
299
307
|
includeTags,
|
|
300
308
|
excludeTags,
|
|
301
|
-
examplePatterns: normalizeExamplePatterns(examplePatterns)
|
|
309
|
+
examplePatterns: normalizeExamplePatterns(examplePatterns),
|
|
310
|
+
retries,
|
|
311
|
+
setupFiles: setupFiles.map((setupFile) => path.resolve(process.cwd(), setupFile)),
|
|
312
|
+
timeoutMs
|
|
302
313
|
}
|
|
303
314
|
}
|
|
304
315
|
|
|
@@ -372,9 +383,18 @@ async function runBrowserTests() {
|
|
|
372
383
|
|
|
373
384
|
await buildBrowserTestApp()
|
|
374
385
|
|
|
375
|
-
const {
|
|
386
|
+
const {
|
|
387
|
+
testFiles,
|
|
388
|
+
lineFilters,
|
|
389
|
+
includeTags,
|
|
390
|
+
excludeTags,
|
|
391
|
+
examplePatterns,
|
|
392
|
+
retries,
|
|
393
|
+
setupFiles,
|
|
394
|
+
timeoutMs
|
|
395
|
+
} = await resolveTests(processArgs)
|
|
376
396
|
const configuration = await resolveConfiguration({
|
|
377
|
-
testFilesRequireContextCallback: async () => createTestFilesRequireContext(testFiles)
|
|
397
|
+
testFilesRequireContextCallback: async () => createTestFilesRequireContext([...setupFiles, ...testFiles])
|
|
378
398
|
})
|
|
379
399
|
const testRunner = new TestRunner({
|
|
380
400
|
configuration,
|
|
@@ -382,7 +402,10 @@ async function runBrowserTests() {
|
|
|
382
402
|
includeTags,
|
|
383
403
|
testFiles,
|
|
384
404
|
lineFilters,
|
|
385
|
-
examplePatterns
|
|
405
|
+
examplePatterns,
|
|
406
|
+
retries,
|
|
407
|
+
setupFiles,
|
|
408
|
+
timeoutMs
|
|
386
409
|
})
|
|
387
410
|
/** @type {BrowserTestSession | undefined} */
|
|
388
411
|
let browserTestSession
|
|
@@ -18,6 +18,7 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
|
|
|
18
18
|
|
|
19
19
|
const APPROVED_BASE_DIGEST = "sha256:3131b4cc82a783df6c9df078f86e01819a13594b865c2cad47bd1bca2b7063bb"
|
|
20
20
|
const PROVIDER_PACKAGES = ["@moonshot-ai/kimi-code", "@openai/codex", "@anthropic-ai/claude-code", "opencode-ai"]
|
|
21
|
+
const QWEN_CODE_SPEC = "@qwen-code/qwen-code@0.23.3"
|
|
21
22
|
const COMPOSE_CREDENTIAL_VALUE_PATTERN = /^\s+[A-Z][A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIALS?)[A-Z0-9_]*:\s*\S.*$/mu
|
|
22
23
|
|
|
23
24
|
// The complete universal apt coding/debugging baseline shared with the
|
|
@@ -198,7 +199,18 @@ function verifyDockerfile(content) {
|
|
|
198
199
|
problems.push("Missing --allow-scripts lifecycle-script allowlisting")
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
|
|
202
|
+
if (!content.includes("@qwen-code/audio-capture")) {
|
|
203
|
+
problems.push("Missing Qwen audio-capture lifecycle-script allowlisting")
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!content.includes(`"${QWEN_CODE_SPEC}"`)) {
|
|
207
|
+
problems.push(`Missing owner-pinned Qwen Code install spec: ${QWEN_CODE_SPEC}`)
|
|
208
|
+
}
|
|
209
|
+
if (!content.includes('test "$(qwen --version)" = "0.23.3"')) {
|
|
210
|
+
problems.push("Missing exact Qwen Code 0.23.3 verification")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const probe of ["kimi --version", "codex --version", "claude --version", "opencode --version", "qwen --version"]) {
|
|
202
214
|
if (!content.includes(probe)) {
|
|
203
215
|
problems.push(`Missing provider CLI command probe: ${probe}`)
|
|
204
216
|
}
|
package/src/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
|
}
|